diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
index 075d2b5b..a300205e 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -12,25 +12,15 @@ assignees: ''
๐ Hi there!
Thanks for using metrics and helping us to improve!
- Please check the following before filling a bug report:
- - It does not duplicate another existing issue
- - Retry at least once to confirm that it was not some random error
+ Please:
+ - Check you're not duplicating an existing issue
+ - Provide a clear and concise description
- For visual issues, mispelled words, etc. ...
- - Provide a description of what you expected to happen
- - Optionally add screenshots or additional context
+ For workflows errors:
+ - Retry at least once to confirm that error is reproductible
+ - Paste an excerpt of your workflow step and error logs
- For runtime errors...
- impacting action version:
- - Paste an excerpt of:
- - workflow step
- - error logs
- - direct GitHub links to the above
-
- impacting web instance version:
- - Paste used url query
-
- For other issues...
- - Just write a clear and concise description of what the bug is
+ For web instance errors:
+ - Paste used url query
-->
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
index 9fd02786..084e0b2d 100644
--- a/.github/ISSUE_TEMPLATE/feature_request.md
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -12,16 +12,10 @@ assignees: ''
๐ Hi there!
Thanks for using metrics and helping us to improve!
- Please check the following before filling a feature request:
- - It does not duplicate another existing issue
+ Please:
+ - Check you're not duplicating an existing issue
- It is not mentioned in https://github.com/lowlighter/metrics/projects/1
-
- For plugin requests...
- - Add "plugin" label
- - Optionally add screenshots or additional context
-
- For other requests...
- - Just write a clear and concise description about the feature request
- - Optionally add screenshots or additional context
+ - Add correct labeling
+ - Provide a clear and concise description
-->
diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md
index e59f5c52..23bb8b57 100644
--- a/.github/ISSUE_TEMPLATE/question.md
+++ b/.github/ISSUE_TEMPLATE/question.md
@@ -12,21 +12,9 @@ assignees: ''
๐ Hi there!
Thanks for using metrics!
- Before asking a question or for help, try to:
- - Search for a similar already existing issue
- - Check README.md documentation
- - Note that most of documentation is collapsed by default, so be sure to open sections marked with "โถ",
- the solution to your problem may already be present!
-
- For setup help...
- - Be sure to create required secrets (METRICS_TOKEN and other plugins token when needed)
- - Paste an excerpt of:
- - workflow step
- - error logs (if applicable)
- - direct GitHub links to the above
- - Optionally add screenshots or additional context
-
- For other questions...
- - Just write about what you want to talk!
+ Please:
+ - Search for similar issues
+ - Check documentation
+ - Provide a clear and concise description
-->
diff --git a/.github/config/label.yml b/.github/config/label.yml
index 0f930cfb..91e503cc 100644
--- a/.github/config/label.yml
+++ b/.github/config/label.yml
@@ -3,7 +3,6 @@ repository:
- .github/**
- .gitignore
- .gitattributes
- - README.md
- SECURITY.md
- LICENSE
- CONTRIBUTING.md
@@ -15,22 +14,17 @@ docker:
# Metrics source code editions
core:
- - source/app/metrics.mjs
- - source/app/setup.mjs
+ - source/app/metrics/**
action:
- source/app/action/**
- - action.yml
web:
- source/app/web/**
- - settings.example.json
plugins:
- source/plugins/**
-queries:
- - source/queries/**
templates:
- source/templates/**
tests:
- - source/app/mocks.mjs
+ - source/app/mocks/**
- tests/**
dependencies:
- package.json
diff --git a/.github/index.mjs b/.github/index.mjs
new file mode 100644
index 00000000..1268c0f9
--- /dev/null
+++ b/.github/index.mjs
@@ -0,0 +1,53 @@
+//Imports
+ import ejs from "ejs"
+ import fs from "fs/promises"
+ import paths from "path"
+ import url from "url"
+ import sgit from "simple-git"
+ import metadata from "../source/app/metrics/metadata.mjs"
+
+//Mode
+ const [mode = "dryrun"] = process.argv.slice(2)
+ console.log(`Mode: ${mode}`)
+
+//Paths
+ const __metrics = paths.join(paths.dirname(url.fileURLToPath(import.meta.url)), "..")
+ const __action = paths.join(__metrics, "source/app/action")
+ const __web = paths.join(__metrics, "source/app/web")
+ const __readme = paths.join(__metrics, ".github/readme")
+
+//Git setup
+ const git = sgit(__metrics)
+ const staged = new Set()
+
+//Load plugins metadata
+ const {plugins, templates} = await metadata({log:false})
+
+//Update generated files
+ async function update({source, output, options = {}}) {
+ //Regenerate file
+ console.log(`Generating ${output}`)
+ const content = await ejs.renderFile(source, {plugins, templates}, {async:true, ...options})
+ //Save result
+ const file = paths.join(__metrics, output)
+ await fs.writeFile(file, content)
+ //Add to git
+ staged.add(file)
+ }
+
+//Rendering
+ await update({source:paths.join(__readme, "README.md"), output:"README.md", options:{root:__readme}})
+ await update({source:paths.join(__readme, "partials/documentation/plugins.md"), output:"source/plugins/README.md"})
+ await update({source:paths.join(__readme, "partials/documentation/templates.md"), output:"source/templates/README.md"})
+ await update({source:paths.join(__action, "action.yml"), output:"action.yml"})
+ await update({source:paths.join(__web, "settings.example.json"), output:"settings.example.json"})
+
+//Commit and push
+ if (mode === "publish") {
+ await git
+ .addConfig("user.name", "GitHub Action")
+ .addConfig("user.email", "<>")
+ .add(...staged)
+ .commit("Auto regenerate files - [Skip GitHub Action]")
+ .push("origin", "master")
+ }
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
index 212714ef..a67bd0e0 100644
--- a/.github/pull_request_template.md
+++ b/.github/pull_request_template.md
@@ -3,38 +3,10 @@
๐ Hi there!
Thanks for contributing to metrics and helping us to improve!
- Please check the following before opening a pull request:
- - It does not duplicate another existing pull request
- - It is not mentioned in https://github.com/lowlighter/metrics/projects/1
- - If it is, ensure that maintainers are aware that you're working on this subject
-
- Then, explain briefly what your pull request is about and link any related issues (if applicable) to help us keeping track.
-
- For documentation updates....
- - Check spelling before asking for a review
- - Respect current formatting (check that your editions blends well with current state)
- - Static images must be saved in /.github/readme/imgs and must be of width 1260px
- - UI should always be set in English in screenshots
-
- For new plugins...
- - Ensure that you created:
- - a plugin entrypoint named index.mjs in /source/plugins
- - tests in /tests/metrics.test.js
- - mocked data if needed (required for all APIs which requires a token or limited in requests)
- - Ensure you updated:
- - /source/app/action/index.mjs to support new plugin options and use correct typing
- - /source/web/statics/* to support new plugin options
- - /settings.example.json with new plugin name
- - README.md to explain new plugin features
- - Include a screenshot in your pull request description
- - You can use `&config.output=png` option in your web instance for it
-
- For all code editions...
- - Ensure retro-compatibility with previous versions (
- - (unless for unreleased features, for which breaking changes are allowed)
- - Respect current formatting
- - Prefers using appropriate single words for variables and plugins names
- - Avoid using uppercases letters, brackets and semicolons when possible to avoid visual pollution
- - Comments should be added before each "code paragraph" and are considered indent worthy
+ Please:
+ - Read CONTRIBUTING.md first
+ - Check you're not duplicating another existing pull request
+ - Add correct labeling
+ - Provide a clear and concise description
-->
diff --git a/.github/readme/README.md b/.github/readme/README.md
new file mode 100644
index 00000000..7f32bddd
--- /dev/null
+++ b/.github/readme/README.md
@@ -0,0 +1,7 @@
+# ๐ Metrics
+
+
+
+<% for (const partial of ["introduction", "shared", "setup", "documentation", "references", "license"]) { -%>
+<%- await include(`/partials/${partial}.md`) %>
+<% } %>
diff --git a/.github/readme/partials/documentation.md b/.github/readme/partials/documentation.md
new file mode 100644
index 00000000..971076f2
--- /dev/null
+++ b/.github/readme/partials/documentation.md
@@ -0,0 +1,5 @@
+# ๐ Documentation
+
+<% for (const partial of ["compatibility", "templates", "plugins", "organizations", "contributing"]) { %>
+<%- await include(`/partials/documentation/${partial}.md`) -%>
+<% } %>
diff --git a/.github/readme/partials/documentation/compatibility.md b/.github/readme/partials/documentation/compatibility.md
new file mode 100644
index 00000000..c5a79764
--- /dev/null
+++ b/.github/readme/partials/documentation/compatibility.md
@@ -0,0 +1,15 @@
+### ๐งฐ Template/plugin compatibily matrix
+
+
+
+
Template\Plugin
<%# -%>
+ <% for (const [plugin, {icon}] of Object.entries(plugins).filter(([key, value]) => (value)&&(!["core"].includes(key)))) { %>
+
<%= icon %>
<% } %>
+
<%# -%>
+ <% for (const [template, {name, readme}] of Object.entries(templates).filter(([key, value]) => (value)&&(!["community"].includes(key)))) { %>
+
+
<%= name %>
<%# -%>
+ <% for (const [plugin] of Object.entries(plugins).filter(([key, value]) => (value)&&(!["core"].includes(key)))) { %>
+
diff --git a/.github/readme/partials/documentation/contributing.md b/.github/readme/partials/documentation/contributing.md
new file mode 100644
index 00000000..f0d880e6
--- /dev/null
+++ b/.github/readme/partials/documentation/contributing.md
@@ -0,0 +1,9 @@
+## ๐ช Customizing and contributing
+
+Metrics is built to be easily customizable.
+Fork this repository, switch used action from `lowlighter/metrics@latest` to your fork and start coding!
+
+To suggest a new feature, report a bug or ask for help, fill an [issue](https://github.com/lowlighter/metrics/issues) describing it.
+
+If you want to contribute, submit a [pull request](https://github.com/lowlighter/metrics/pulls).
+Be sure to read [CONTRIBUTING.md](CONTRIBUTING.md) for more information about this.
diff --git a/.github/readme/partials/documentation/organizations.md b/.github/readme/partials/documentation/organizations.md
new file mode 100644
index 00000000..126ff534
--- /dev/null
+++ b/.github/readme/partials/documentation/organizations.md
@@ -0,0 +1,48 @@
+### ๐ฆ Organizations metrics
+
+While metrics targets mainly user accounts, it's possible to render metrics for organization accounts.
+
+
+
+
+๐ฌ Metrics for organizations
+
+Setup is the same as for user accounts, though you'll need to add `read:org` scope, **whether you're member of target organization or not**.
+
+
+
+You'll also need to set `user` option with your organization name.
+
+If you're encounting errors and your organization is using single sign-on, try to [authorize your personal token](https://docs.github.com/en/free-pro-team@latest/github/authenticating-to-github/authorizing-a-personal-access-token-for-use-with-saml-single-sign-on).
+
+Most of plugins supported by user accounts will work with organization accounts, but note that rendering metrics for organizations consume way more APIs requests.
+
+To support private repositories, add full `repo` scope to your personal token.
+
+#### โน๏ธ Example workflow
+
+```yaml
+- uses: lowlighter/metrics@latest
+ with:
+ # ... other options
+ token: ${{ secrets.METRICS_TOKEN }} # A personal token from an user account with read:org scope
+ committer_token: ${{ secrets.GITHUB_TOKEN }} # GitHub auto-generated token
+ user: organization-name # Organization name
+```
+
+
+
+
+๐ฌ Organizations memberships for user accounts
+
+Only public memberships can be displayed by metrics by default.
+You can manage your membership visibility in the `People` tab of your organization:
+
+
+
+For organization memberships, add `read:org` scope to your personal token.
+
+
+
+
+
diff --git a/.github/readme/partials/documentation/plugins.md b/.github/readme/partials/documentation/plugins.md
new file mode 100644
index 00000000..812f2fe3
--- /dev/null
+++ b/.github/readme/partials/documentation/plugins.md
@@ -0,0 +1,7 @@
+## ๐งฉ 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:
+<% for (const [plugin, {name}] of Object.entries(plugins).filter(([key, value]) => value)) { %>
+* [<%= name %>](/source/plugins/<%= plugin %>/README.md)<%# -%>
+<% } %>
diff --git a/.github/readme/partials/documentation/templates.md b/.github/readme/partials/documentation/templates.md
new file mode 100644
index 00000000..41c1fab8
--- /dev/null
+++ b/.github/readme/partials/documentation/templates.md
@@ -0,0 +1,7 @@
+## ๐ผ๏ธ Templates
+
+Templates lets you change general appearance of rendered metrics.
+See their respective documentation for more informations about how to setup them:
+<% for (const [template, {name}] of Object.entries(templates).filter(([key, value]) => value)) { %>
+* [<%= name %>](/source/templates/<%= template %>/README.md)<%# -%>
+<% } %>
diff --git a/.github/readme/partials/introduction.md b/.github/readme/partials/introduction.md
new file mode 100644
index 00000000..6feb2db0
--- /dev/null
+++ b/.github/readme/partials/introduction.md
@@ -0,0 +1,80 @@
+Generate your metrics that you can embed everywhere, including your GitHub profile readme! It works for both user and organization accounts, and even for repositories!
+
+
+
+
For user accounts
+
For organization accounts
+
+
+ <%- plugins.base.readme.demo?.replace(/
+
+
+<% {
+ let cell = 0
+ const elements = Object.entries(plugins).filter(([key, value]) => (value)&&(!["base", "core"].includes(key)))
+ if (elements.length%2)
+ elements.push(["", {}])
+%>
+
+<% }}
+ for (const [cell, [template, {name, readme}]] of cells) {
+ if (cell === "even") {
+-%>
+
+<% } %> <%- readme.demo.replace(/ i ? ` ${x}` : x)?.join("\n") %>
+<% if (cell === "odd") {
+-%>
+<% }}} -%>
+
+<% } %>
diff --git a/.github/readme/partials/license.md b/.github/readme/partials/license.md
new file mode 100644
index 00000000..70962eec
--- /dev/null
+++ b/.github/readme/partials/license.md
@@ -0,0 +1,6 @@
+## ๐ License
+
+```
+MIT License
+Copyright (c) 2020 lowlighter
+```
diff --git a/.github/readme/partials/references.md b/.github/readme/partials/references.md
new file mode 100644
index 00000000..6753180a
--- /dev/null
+++ b/.github/readme/partials/references.md
@@ -0,0 +1,15 @@
+## ๐ Useful references
+
+* [GitHub GraphQL API](https://docs.github.com/en/graphql)
+* [GitHub GraphQL Explorer](https://docs.github.com/en/free-pro-team@latest/graphql/overview/explorer)
+* [GitHub Rest API](https://docs.github.com/en/rest)
+* [GitHub Octicons](https://github.com/primer/octicons)
+ * See [GitHub Logos and Usage](https://github.com/logos) for more information.
+
+### โจ Inspirations
+
+* [anuraghazra/github-readme-stats](https://github.com/anuraghazra/github-readme-stats)
+* [jstrieb/github-stats](https://github.com/jstrieb/github-stats)
+* [ankurparihar/readme-pagespeed-insights](https://github.com/ankurparihar/readme-pagespeed-insights)
+* [jasonlong/isometric-contributions](https://github.com/jasonlong/isometric-contributions)
+* [jamesgeorge007/github-activity-readme](https://github.com/jamesgeorge007/github-activity-readme)
diff --git a/.github/readme/partials/setup.md b/.github/readme/partials/setup.md
new file mode 100644
index 00000000..1316579a
--- /dev/null
+++ b/.github/readme/partials/setup.md
@@ -0,0 +1,5 @@
+# ๐ How to use?
+
+<% for (const partial of ["action", "shared", "web"]) { -%>
+<%- await include(`/partials/setup/${partial}.md`) %>
+<% } %>
diff --git a/.github/readme/partials/setup/action.md b/.github/readme/partials/setup/action.md
new file mode 100644
index 00000000..57a7cd82
--- /dev/null
+++ b/.github/readme/partials/setup/action.md
@@ -0,0 +1,113 @@
+## โ๏ธ Using GitHub Action on your profile repository (~5 min setup)
+
+Setup a GitHub Action which runs periodically and pushes your generated metrics image to your repository.
+See all supported options in [action.yml](action.yml).
+
+Assuming your username is `my-github-user`, you can then embed rendered metrics in your readme like below:
+
+```markdown
+
+
+
+
+```
+
+
+๐ฌ How to setup?
+
+### 0. Setup your personal repository
+
+Create a repository with the same name as your GitHub login (if it's not already done).
+
+
+
+Its `README.md` will be displayed on your user profile:
+
+
+
+### 1. Create a GitHub personal token
+
+From the `Developer settings` of your account settings, select `Personal access tokens` to create a new token.
+
+No additional scopes are needed for basic metrics, but you may have to grant additional scope depending on what features you're planning to use:
+- `public_repo` scope for some plugins
+- `read:org` scope for all organizations related metrics
+- `repo` scope for all private repositories related metrics
+
+
+
+A scope-less token can still display private contributions by enabling `Include private contributions on my profile` in your account settings:
+
+
+
+If a plugin has not enough scopes to operate (and `plugins_errors_fatal` isn't enabled), it'll be reported in the rendering like below:
+
+
+
+### 2. Put your GitHub perosnal token in your repository secrets
+
+Go to the `Settings` of your repository to create a new secret and paste your freshly generated GitHub token there.
+
+
+
+### 3. Create a GitHub Action workflow in your repository
+
+Create a new workflow from the `Actions` tab of your repository and paste the following:
+
+```yaml
+name: Metrics
+on:
+ # Schedule updates (each hour)
+ schedule: [{cron: "0 * * * *"}]
+ # Lines below let you run workflow manually and on each commit (optional)
+ push: {branches: ["master", "main"]}
+ workflow_dispatch:
+jobs:
+ github-metrics:
+ runs-on: ubuntu-latest
+ steps:
+ # See action.yml for all options
+ - uses: lowlighter/metrics@latest
+ with:
+ # Your GitHub token
+ token: ${{ secrets.METRICS_TOKEN }}
+ # GITHUB_TOKEN is a special auto-generated token restricted to current repository, which is used to push files in it
+ committer_token: ${{ secrets.GITHUB_TOKEN }}
+```
+
+See all supported options in [action.yml](action.yml).
+
+Rendered metrics will be committed to your repository on each run.
+
+
+
+#### Choosing between `@latest`, `@master` or a fork
+
+If you wish to use new features as they're being released, you can switch from `@latest` to `@master`.
+As the latter is used as a development branch, jobs may fail from time to time (although we try to mitigate this).
+
+When using a token with additional permissions, it is advised to fork this repository and use it instead to minimize security risks:
+```yaml
+ - uses: my-github-username/metrics@master
+```
+
+In this case, please consider watching new releases to stay up-to-date and enjoy latest features!
+
+`@latest` will be updated on each release soon after [Planned for next release](https://github.com/lowlighter/metrics/projects/1#column-12378679) is emptied. Metrics doesn't introduce breaking changes **from an user point of view** (i.e. your workflows will always be valid) so you can follow release cycles without worrying.
+
+#### Examples workflows
+
+Metrics displayed on this page are rendered from this [workflow](https://github.com/lowlighter/lowlighter/blob/master/.github/workflows/metrics.yml). You can check it for some code examples.
+
+### 4. Embed link into your README.md
+
+Update your README.md to embed your metrics:
+
+```markdown
+
+
+
+
+```
+
+
diff --git a/.github/readme/partials/setup/shared.md b/.github/readme/partials/setup/shared.md
new file mode 100644
index 00000000..4d334231
--- /dev/null
+++ b/.github/readme/partials/setup/shared.md
@@ -0,0 +1,21 @@
+## ๐ Using the shared instance (~1 min setup, but with limitations)
+
+For convenience, you can use the shared instance available at [metrics.lecoq.io](https://metrics.lecoq.io) without any additional setup.
+
+```markdown
+
+```
+
+This is mostly intended for previews, to enjoy all features consider using GitHub Action instead.
+
+
+๐ฌ Fair use
+
+To ensure service availability, shared instance has a few limitations:
+ * Images are cached for 1 hour
+ * Rendered metrics **won't be updated** during this time window when queried
+ * You can manually update rendering againg your metrics on [metrics.lecoq.io](https://metrics.lecoq.io)
+ * There is a rate limiter enabled (it doesn't affect already cached metrics)
+ * Several plugins may not be available
+
+
diff --git a/.github/readme/partials/setup/web.md b/.github/readme/partials/setup/web.md
new file mode 100644
index 00000000..8911887f
--- /dev/null
+++ b/.github/readme/partials/setup/web.md
@@ -0,0 +1,140 @@
+## ๐๏ธ Deploying your own web instance (~15 min setup, depending on your sysadmin knowledge)
+
+
+Setup a metrics instance on your server if you don't want to use GitHub Actions and [metrics.lecoq.io](https://metrics.lecoq.io).
+See all supported options in [settings.example.json](settings.example.json).
+
+Assuming your username is `my-github-user`, you can then embed rendered metrics in your readme like below:
+
+```markdown
+
+```
+
+
+๐ฌ How to setup?
+
+### 0. Prepare your server
+
+You'll need a server with a recent version [NodeJS](https://nodejs.org) (see used version in [Dockerfile](Dockerfile#L1-L2)).
+
+### 1. Create a GitHub personal token
+
+From the `Developer settings` of your account settings, select `Personal access tokens` to create a new token.
+
+No additional scopes are needed.
+
+
+
+### 2. Install dependencies
+
+Clone repository, install dependencies and copy configuration example to `settings.json`:
+
+```shell
+git clone https://github.com/lowlighter/metrics.git
+cd metrics/
+npm install --only=prod
+cp settings.example.json settings.json
+```
+
+### 3. Configure your instance and start it
+
+Edit `settings.json` to configure your instance.
+
+```javascript
+{
+ //GitHub API token
+ "token":"GITHUB_TOKEN",
+ //Other options...
+}
+```
+
+See all supported options in [settings.example.json](settings.example.json).
+
+If you plan to make your web instance public, it is advised to restrict its access thanks to rate limiter and access list.
+
+Once you've finished configuring metrics, start your instance:
+
+```shell
+npm start
+```
+
+Access your server with provided port in `setting.json` from your browser to ensure everything is working.
+
+### 4. Embed link into your README.md
+
+Edit your repository readme and add your metrics image from your server domain:
+
+```markdown
+
+```
+
+### 6. (optional) Setup your instance as a service
+
+To ensure that your instance will restart if it reboots or crashes, you should set it up as a service.
+This is described below for Linux-like systems which support *systemd*.
+
+Create a new service file `/etc/systemd/system/github_metrics.service` and paste the following after editing paths inside:
+
+```ini
+[Unit]
+Description=Metrics
+After=network-online.target
+Wants=network-online.target
+
+[Service]
+Type=simple
+WorkingDirectory=/path/to/metrics
+ExecStart=/usr/bin/node /path/to/metrics/index.mjs
+
+[Install]
+WantedBy=multi-user.target
+```
+
+Reload services, enable it, start it and check if it is up and running:
+
+```shell
+systemctl daemon-reload
+systemctl enable github_metrics
+systemctl start github_metrics
+systemctl status github_metrics
+```
+
+
+
+
+โ ๏ธ HTTP errors code
+
+Following error codes may be encountered on web instance:
+
+| Error code | Description |
+| ------------------------- | -------------------------------------------------------------------------- |
+| `400 Bad request` | Invalid query (e.g. unsupported template) |
+| `403 Forbidden` | User not allowed in `restricted` users list |
+| `404 Not found` | GitHub API did not found the requested user |
+| `429 Too many requests` | Thrown when rate limiter is trigerred |
+| `500 Internal error` | Server error while generating metrics images (check logs for more details) |
+| `503 Service unavailable` | Maximum user capacity reached, only cached images can be accessed for now |
+
+
+
+
+๐ HTTP parameters
+
+Most of options from [action.yml](action.yml) are actually supported by web instance, though syntax is slightly different.
+All underscores (`_`) must be replaced by dots (`.`) and `plugin_` prefixes must be dropped.
+
+For example, to configure pagespeed plugin you'd use the following:
+```
+https://my-personal-domain.com/my-github-user?pagespeed=1&pagespeed.detailed=1&pagespeed.url=https%3A%2F%2Fexample.com
+```
+
+Note that url parameters must be [encoded](https://developer.mozilla.org/fr/docs/Web/JavaScript/Reference/Objets_globaux/encodeURIComponent).
+
+As for `base` content, which is enabled by default, sections are available through "`base.`".
+
+For example, to display only `repositories` section, use:
+```
+https://my-personal-domain.com/my-github-user?base=0&base.repositories=1
+```
+
+
diff --git a/.github/readme/partials/shared.md b/.github/readme/partials/shared.md
new file mode 100644
index 00000000..20956772
--- /dev/null
+++ b/.github/readme/partials/shared.md
@@ -0,0 +1,5 @@
+## ๐ฆ Interested to get your own?
+
+Try it now at [metrics.lecoq.io](https://metrics.lecoq.io/) with your GitHub username!
+
+Some plugins are not are not available at [metrics.lecoq.io](https://metrics.lecoq.io/), for a fully-featured experience use it as a [GitHub Action](https://github.com/marketplace/actions/github-metrics-as-svg-image) instead!
diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml
index 96771b04..147800e1 100644
--- a/.github/workflows/workflow.yml
+++ b/.github/workflows/workflow.yml
@@ -73,11 +73,29 @@ jobs:
verify: yes
use_prebuilt_image: master
+ # Update plugins and template indexes, along with README.md
+ update-indexes:
+ name: Publish rebuilt metrics indexes
+ runs-on: ubuntu-latest
+ needs: [ action-master-test ]
+ if: github.event_name == 'push' && github.ref == 'refs/heads/master'
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v2
+ - name: Setup NodeJS
+ uses: actions/setup-node@v2
+ with:
+ node-version: 15
+ - name: Setup metrics
+ run: npm ci
+ - name: Publish rebuild metrics indexes
+ run: npm run index -- publish
+
# Build docker image from master and publish it to GitHub registry with release tag
docker-release:
name: Publish release to GitHub registry
runs-on: ubuntu-latest
- needs: [ build, analyze, action-master-test ]
+ needs: [ build, analyze, action-master-test, update-indexes ]
if: github.event_name == 'push' && github.ref == 'refs/heads/master' && contains(github.event.head_commit.message, '[release]')
steps:
- name: Checkout repository
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 6128da1f..648a916e 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -101,13 +101,13 @@ Review below which contributions are accepted:
@@ -116,11 +116,11 @@ Review below which contributions are accepted:
**Legend**
* โ๏ธ: Contributions welcomed!
* โญ: Contributions welcomed, but must be discussed first with a maintainer
-* โ: Only maintainers can edit these files
+* โ: Only maintainers can manage these files
Before working on something, ensure that it isn't listed in [In progress](https://github.com/lowlighter/metrics/projects/1#column-12158618) and that no open pull requests (including drafts) already implement what you want to do.
-If it's listed in [Roadmap and todos](https://github.com/lowlighter/metrics/projects/1) be sure to let maintainers that you're working on it. As metrics remains a side projects, things being working can change from one day to another.
+If it's listed in [Roadmap and todos](https://github.com/lowlighter/metrics/projects/1) be sure to let maintainers that you're working on it. As metrics remains a side project, things being working on can change from one day to another.
If you're unsure, always open an issue to obtain insights and feedback ๐
@@ -129,42 +129,43 @@ Metrics is designed to be highly customizable, so you can always decide to gener
-
๐๏ธ Project structure
This section explain how metrics is structured.
-**Metrics generator**
-
-* `source/app/setup.mjs` contains configuration setup
-* `source/app/mocks.mjs` contains mocked data used for tests
-* `source/app/metrics.mjs` contains the metrics renderer
-* `source/plugins/*` contains source code of plugins
-* `source/queries/*` contains GraphQL queries
-* `source/templates/*` contains templates files
-* `source/templates/*/image.svg` contains the image template used to render metrics
-* `source/templates/*/style.css` contains the style used to render metrics
-* `source/templates/*/fonts.css` contains additional fonts used to render metrics
-* `source/templates/*/template.mjs` contains the code used to prepare metrics data before rendering
-
-**Web instance**
-
-* `source/app/web/index.mjs` contains metrics web instance entry point
-* `source/web/instance.mjs` contains metrics web instance source code
-* `source/app/web/statics/*` contains metrics web instance static files
-* `settings.example.json` contains metrics web instance settings example
-
-**GitHub action**
-
-* `action.yml` contains GitHub action descriptor
-* `source/app/action/index.mjs` contains GitHub action source code
-
-**Others**
-
-* `tests/metrics.test.js` contains tests
-* `Dockerfile` contains the docker instructions to build metrics image
-* `package.json` contains dependencies lists of metrics along with command line aliases
+* `source/app/metrics/` contains core metrics files
+* `source/app/action/` contains GitHub action files
+ * `index.mjs` contains GitHub action entry point
+ * `action.yml` contains GitHub action descriptor
+* `source/app/web/` contains web instance files
+ * `index.mjs` contains web instance entry point
+ * `instance.mjs` contains web instance source code
+ * `settings.example.json` contains web instance settings example
+ * `statics/` contains web instance static files
+ * `app.js` contains web instance client source code
+ * `app.placeholder.js` contains web instance placeholder mocked data
+* `source/app/mocks/` contains mocked data files
+ * `api/` contains mocked api data
+ * `axios/` contains external REST APIs mocked data
+ * `github/` contains mocked GitHub api data
+ * `index.mjs` contains mockers
+* `source/plugins/` contains source code of plugins
+ * `README.md` contains plugin documentation
+ * `metadata.yml` contains plugin metadata
+ * `index.mjs` contains plugin source code
+ * `queries/` contains plugin GraphQL queries
+* `source/templates/` contains templates files
+ * `README.md` contains template documentation
+ * `image.svg` contains template image used to render metrics
+ * `style.css` contains style used to render metrics
+ * `fonts.css` contains additional fonts used to render metrics
+ * `template.mjs` contains template source code
+* `tests/` contains tests
+ * `testscases.js` contains tests case
+ * `metrics.test.js` contains metrics testers
+* `Dockerfile` contains docker instructions used to build metrics image
+* `package.json` contains dependencies and command line aliases
@@ -197,12 +198,12 @@ Below is a list of used packages.
* To scrap the web
* [libxmljs/libxmljs](https://github.com/libxmljs/libxmljs)
* To test and verify SVG validity
-* [marak/colors.js](https://github.com/marak/colors.js)
- * To print colors in console
* [facebook/jest](https://github.com/facebook/jest) and [nodeca/js-yaml](https://github.com/nodeca/js-yaml)
* For unit testing
-* [marak/faker.js](https://github.com/marak/Faker.js/)
+* [marak/faker.js](https://github.com/marak/Faker.js)
* For mocking data
+* [steveukx/git-js](https://github.com/steveukx/git-js)
+ * For simple git operations
@@ -212,18 +213,9 @@ Below is a list of used packages.
Templates requires you to be comfortable with HTML, CSS and JavaScript ([EJS](https://github.com/mde/ejs) flavored).
-Metrics does not really accept contributions on [default templates](https://github.com/lowlighter/metrics/tree/master/source/templates) to keep consistency and avoid bloating main repository with lots of templates, but fear not! It's really easy to create your own template.
+Metrics does not really accept contributions on [default templates](https://github.com/lowlighter/metrics/tree/master/source/templates) in order to avoid bloating main repository with a lot of templates, but fear not! Users will still be able to use your custom templates thanks to [community templates](source/templates/community)!
-If you make something awesome, don't hesistate to share it so user can use it!
-
-Assuming you created a new template named `your-custom-template`, other users will be able to use this by using the following in their workflow:
-```yaml
-- uses: lowlighter/metrics@master
- with:
- # ... other options
- template: "@your-custom-template"
- setup_community_templates: your-github-login/metrics@master:your-custom-template
-```
+If you make something awesome, don't hesistate to share it!
๐ฌ Creating a new template from scratch
@@ -231,6 +223,7 @@ Assuming you created a new template named `your-custom-template`, other users wi
Find a cool name for your template and create an eponym folder in [`source/templates`](https://github.com/lowlighter/metrics/tree/master/source/templates).
Then, you'll need to create the following files:
+- `README.md` will contain template description and documentation
- `image.svg` will contain the base render structure of your template
- `partials/` is a folder that'll contain parts of your template (called "partials")
- `partials/_.json` is a JSON array which lists your partials (these will be displayed in the same order as listed, unless if overriden by user with `config_order` option)
@@ -246,6 +239,36 @@ Note that by default, `template.mjs` is skipped when using official release with
+
+๐ฌ Creating a README.md
+
+Your `README.md` will document your template and explain how it works.
+It must contain at least the following:
+```markdown
+### ๐ My custom template
+
+
+
+
+
+
+
+
+#### โน๏ธ Examples workflows
+
+'''yaml
+- uses: lowlighter/metrics@latest
+ with:
+ # ... other options
+ setup_community_templates: user/metrics@master:template
+ template: "@template"
+'''
+
+```
+
+
+
+
๐ฌ Creating image.svg
@@ -296,17 +319,17 @@ Basically, you can use JavaScript statements in templating tags (`<% %>`) to dis
This is actually not recommended because it drastically increases the size of generated metrics, but it should also make your rendering more consistant. The trick is to actually restrict the charset used to keep file size small.
Below is a simplified process on how to generate base64 encoded fonts to use in metrics:
-1. Find a font on [fonts.google.com](https://fonts.google.com/)
- - Select regular, bold, italic and bold+italic fonts
- - Open `embed` tab and extract the `href`
-2. Open extracted `href` and append `&text=` params with used characters from SVG
- - e.g. `&text=%26%27"%7C%60%5E%40ยฐ%3F!%23%24%25()*%2B%2C-.%2F0123456789%3A%3B<%3D>ABCDEFGHIJKLMNOPQRSTUVWXYZ%5B%5D_abcdefghijklmnopqrstuvwxyz%7B%7D~โโโโโกโ`
-3. Download each font file from url links from the generated stylesheet
-4. Convert them into base64 with `woff` extension on [transfonter.org]https://transfonter.org/) and download archive
-5. Extract archive and copy the content of the generated stylesheet to `fonts.css`
-6. Update your template
- - Include `` to your `image.svg`
- - Edit your `style.css` to use yout new font
+- 1. Find a font on [fonts.google.com](https://fonts.google.com/)
+ - Select regular, bold, italic and bold+italic fonts
+ - Open `embed` tab and extract the `href`
+- 2. Open extracted `href` and append `&text=` params with used characters from SVG
+ - e.g. `&text=%26%27"%7C%60%5E%40ยฐ%3F!%23%24%25()*%2B%2C-.%2F0123456789%3A%3B<%3D>ABCDEFGHIJKLMNOPQRSTUVWXYZ%5B%5D_abcdefghijklmnopqrstuvwxyz%7B%7D~โโโโโกโ`
+- 3. Download each font file from url links from the generated stylesheet
+- 4. Convert them into base64 with `woff` extension on [transfonter.org]https://transfonter.org/) and download archive
+- 5. Extract archive and copy the content of the generated stylesheet to `fonts.css`
+- 6. Update your template
+ - Include `` to your `image.svg`
+ - Edit your `style.css` to use yout new font
@@ -393,7 +416,7 @@ Plugins are autoloaded so you do not need to do anything special to register the
For GitHub related data, always try to use their [GraphQL API](https://docs.github.com/en/graphql) or their [REST API](https://docs.github.com/en/rest) when possible. Use `puppeteer` in last resort.
-When using GraphQL API, `queries` object autoloads queries from [source/queries](https://github.com/lowlighter/metrics/tree/master/source/queries) and will replace all strings prefixed by a dollar sign (`$`) with eponym variables.
+When using GraphQL API, `queries` object autoloads queries from your plugin `queries` directory and will replace all strings prefixed by a dollar sign (`$`) with eponym variables.
For example:
```js
@@ -481,26 +504,129 @@ http://localhost:3000/your-github-login?base=0&your-plugin-name=1
-๐ฌ Registering plugin options inputs
+๐ฌ Registering plugin options in metadata.yml
- ๐ง This section is not available yet
+`metadata.yml` is a special file that will be used to parse user inputs and to generate final `action.yml`
+
+```yaml
+name: "๐งฉ Your plugin name"
+
+# Estimate of how many GitHub requests will be used
+cost: N/A
+
+# Supported modes
+supports:
+ - user
+ - organization
+ - repository
+
+# Inputs list
+inputs:
+
+ # Enable or disable plugin
+ plugin_custom:
+ description: Your custom plugin
+ type: boolean
+ default: no
+```
+
+The following types are supported:
+```yaml
+string:
+ type: string
+
+select:
+ type: string
+ values:
+ - allowed-value-1
+ - allowed-value-2
+ - ...
+
+boolean:
+ type: boolean
+
+number:
+ type: number
+
+ranged:
+ type: number
+ min: 0
+ max: 100
+
+array:
+ type: array
+ format: comma-separated
+
+array_select:
+ type: array
+ format: comma-separated
+ values:
+ - allowed-value-1
+ - allowed-value-2
+ - ...
+
+json:
+ type: json
+```
๐ฌ Create mocked data and tests
- ๐ง This section is not available yet
+Creating tests for your plugin ensure that external changes don't break it.
+
+You can define your tests cases in [`testscases.js`](/tests/testscases.js), which will automatically test your plugin with:
+ - Metrics action
+ - Metrics web instance
+ - Metrics web instance placeholder (rendered by browser)
+
+As most of APIs (including GitHub) usually have a rate-limit to ensure quality of their service.
+To bypass these restrictions but still perform tests, you must mock their data which simulates APIs call returns.
+
+Add them in [`source/app/mocks/api/`](/source/app/mocks/api) folder.
+
+If you're using `axios` or GitHub GraphQL API, these files are autoloaded so you just need to create new functions (see other mocked data for examples).
+
+If you're using GitHub REST API, you'll also need to edit [`source/app/mocks/index.mjs`](/source/app/mocks/index.mjs) to add a new proxied method.
-๐ฌ Updating README.md
+๐ฌ Creating a README.md
- ๐ง This section is not available yet
+Your `README.md` will document your plugin and explain how it works.
+It must contain at least the following:
+
+```markdown
+### ๐งฉ Your plugin name
+
+
+
+
+
+
+
+
+#### โน๏ธ Examples workflows
+
+[โก๏ธ Available options for this plugin](metadata.yml)
+
+'''yaml
+- uses: lowlighter/metrics@latest
+ with:
+ # ... other options
+ plugin_gists: yes
+'''
+
+```
+
+Note that you **must** keep `
` tags as these will be extracted to autogenerated global `README.md` with your example.
+___
+
Written by [lowlighter](https://github.com/lowlighter)
diff --git a/Dockerfile b/Dockerfile
index 3c48fd20..2fe63407 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -27,7 +27,9 @@ RUN chmod +x /metrics/source/app/action/index.mjs \
&& apt-get install -y python3 \
# Install node modules
&& cd /metrics \
- && npm ci
+ && npm ci \
+ # Rebuild indexes
+ && npm run index
# Environment variables
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD true
diff --git a/README.md b/README.md
index 68431887..5ff1f581 100644
--- a/README.md
+++ b/README.md
@@ -2,310 +2,261 @@

-Generate your metrics that you can embed everywhere, including your GitHub profile readme! It works for both user and organization accounts!
+Generate your metrics that you can embed everywhere, including your GitHub profile readme! It works for both user and organization accounts, and even for repositories!
-
For user accounts
-
For organization accounts
+
For user accounts
+
For organization accounts
-
-
+
+
+
-
-
+
+
+
-But there's more with [plugins](https://github.com/lowlighter/metrics/tree/master/source/plugins) and [templates](https://github.com/lowlighter/metrics/tree/master/source/templates)!
-
+
## ๐ฆ Interested to get your own?
Try it now at [metrics.lecoq.io](https://metrics.lecoq.io/) with your GitHub username!
-Because certain plugins require additional configuration and setup, some of them are not available at [metrics.lecoq.io](https://metrics.lecoq.io/).
-For a fully-featured experience, consider using this as a [GitHub Action](https://github.com/marketplace/actions/github-metrics-as-svg-image) instead!
+Some plugins are not are not available at [metrics.lecoq.io](https://metrics.lecoq.io/), for a fully-featured experience use it as a [GitHub Action](https://github.com/marketplace/actions/github-metrics-as-svg-image) instead!
# ๐ How to use?
## โ๏ธ Using GitHub Action on your profile repository (~5 min setup)
-Set up a GitHub Action which runs periodically and pushes your generated metrics image on your repository.
+Setup a GitHub Action which runs periodically and pushes your generated metrics image to your repository.
See all supported options in [action.yml](action.yml).
-Assuming your username is `my-github-user`, you can then embed your metrics in your repository readme like below:
+Assuming your username is `my-github-user`, you can then embed rendered metrics in your readme like below:
```markdown
+

-```
-
-(or, for a new repository with a default branch of `main`):
-
-```markdown
+

```
๐ฌ How to setup?
-### 0. Prepare your personal repository
+### 0. Setup your personal repository
-Create a repository with the same name as your GitHub username if it's not already done.
+Create a repository with the same name as your GitHub login (if it's not already done).

@@ -313,39 +264,41 @@ Its `README.md` will be displayed on your user profile:

-### 1. Create a GitHub token
+### 1. Create a GitHub personal token
From the `Developer settings` of your account settings, select `Personal access tokens` to create a new token.
-No additional scopes are needed, unless you want to include your private repositories metrics.
+No additional scopes are needed for basic metrics, but you may have to grant additional scope depending on what features you're planning to use:
+- `public_repo` scope for some plugins
+- `read:org` scope for all organizations related metrics
+- `repo` scope for all private repositories related metrics

-With a scope-less token, you can still display private contributions by enabling `Include private contributions on my profile` in your account settings:
+A scope-less token can still display private contributions by enabling `Include private contributions on my profile` in your account settings:

-Some plugins also require additional scopes, which is indicated in their respective documentation.
-In case your token does not have the required scope (and `plugins_errors_fatal` is not enabled), it will be directly notified in the plugin render like below:
+If a plugin has not enough scopes to operate (and `plugins_errors_fatal` isn't enabled), it'll be reported in the rendering like below:

-### 2. Set your GitHub token in your personal repository secrets
+### 2. Put your GitHub perosnal token in your repository secrets
-Go to the `Settings` of your personal repository to create a new secret and paste your freshly generated GitHub token there.
+Go to the `Settings` of your repository to create a new secret and paste your freshly generated GitHub token there.

-### 3. Create a new GitHub Action workflow on your personal repository
+### 3. Create a GitHub Action workflow in your repository
-Create a new workflow from the `Actions` tab of your personal repository and paste the following:
+Create a new workflow from the `Actions` tab of your repository and paste the following:
```yaml
name: Metrics
on:
- # Schedule updates
+ # Schedule updates (each hour)
schedule: [{cron: "0 * * * *"}]
- # Lines below let you run workflow manually and on each commit
+ # Lines below let you run workflow manually and on each commit (optional)
push: {branches: ["master", "main"]}
workflow_dispatch:
jobs:
@@ -363,35 +316,37 @@ jobs:
See all supported options in [action.yml](action.yml).
-When using a token with additional permissions, it is advised to fork this repository to minimize security risks:
-```yaml
- - uses: my-github-username/metrics@latest
-```
-In this case, consider watching new releases of this repository to stay up-to-date and enjoy latest features!
-
-#### Preview vs release
-
-It is possible to use `@master` instead of `@latest` to use new features before their official release.
-Breaking changes may occur occasionally on `@master`, which could result in your metrics not being generated temporarily.
-
-#### What will happen?
-
-A new metrics image will be generated and committed to your repository on each run.
+Rendered metrics will be committed to your repository on each run.

-#### Workflow examples
+#### Choosing between `@latest`, `@master` or a fork
-Check out this [workflow](https://github.com/lowlighter/lowlighter/blob/master/.github/workflows/metrics.yml) file which generates metrics daily.
+If you wish to use new features as they're being released, you can switch from `@latest` to `@master`.
+As the latter is used as a development branch, jobs may fail from time to time (although we try to mitigate this).
-Note that most of steps presented there are illustrative examples for this readme and are actually not needed to generate your own metrics.
+When using a token with additional permissions, it is advised to fork this repository and use it instead to minimize security risks:
+```yaml
+ - uses: my-github-username/metrics@master
+```
-### 4. Embed the link into your README.md
+In this case, please consider watching new releases to stay up-to-date and enjoy latest features!
-Edit your repository readme and add your metrics image:
+`@latest` will be updated on each release soon after [Planned for next release](https://github.com/lowlighter/metrics/projects/1#column-12378679) is emptied. Metrics doesn't introduce breaking changes **from an user point of view** (i.e. your workflows will always be valid) so you can follow release cycles without worrying.
+
+#### Examples workflows
+
+Metrics displayed on this page are rendered from this [workflow](https://github.com/lowlighter/lowlighter/blob/master/.github/workflows/metrics.yml). You can check it for some code examples.
+
+### 4. Embed link into your README.md
+
+Update your README.md to embed your metrics:
```markdown
+

+
+
```
@@ -400,43 +355,44 @@ Edit your repository readme and add your metrics image:
For convenience, you can use the shared instance available at [metrics.lecoq.io](https://metrics.lecoq.io) without any additional setup.
-Assuming your username is `my-github-user`, you can embed your metrics in your repository readme like below:
-
```markdown

```
-Visit [metrics.lecoq.io](https://metrics.lecoq.io) for more informations.
+This is mostly intended for previews, to enjoy all features consider using GitHub Action instead.
-๐ฌ Restrictions and fair use
+๐ฌ Fair use
-Since GitHub API has rate limitations, the shared instance has a few limitations:
+To ensure service availability, shared instance has a few limitations:
* Images are cached for 1 hour
- * Your generated metrics won't be updated during this amount of time when queried
- * A rate limiter is enabled, although it won't affect already cached users metrics
- * Plugins which consume additional requests, or require additional token scopes are disabled
-
-If you're appreciating this project, consider using it as a GitHub Action instead.
+ * Rendered metrics **won't be updated** during this time window when queried
+ * You can manually update rendering againg your metrics on [metrics.lecoq.io](https://metrics.lecoq.io)
+ * There is a rate limiter enabled (it doesn't affect already cached metrics)
+ * Several plugins may not be available
## ๐๏ธ Deploying your own web instance (~15 min setup, depending on your sysadmin knowledge)
-It is possible to set up your own instance if you don't want to use GitHub Actions or if you want to allow others users on your instance.
-
-When sharing an instance, it is advised to restrict the number of users which can use it through the rate limiter or the access list, to avoid reaching the requests limit of GitHub APIs.
+Setup a metrics instance on your server if you don't want to use GitHub Actions and [metrics.lecoq.io](https://metrics.lecoq.io).
See all supported options in [settings.example.json](settings.example.json).
+Assuming your username is `my-github-user`, you can then embed rendered metrics in your readme like below:
+
+```markdown
+
+```
+
๐ฌ How to setup?
### 0. Prepare your server
-You will need a server where you can set up and run a NodeJS application.
+You'll need a server with a recent version [NodeJS](https://nodejs.org) (see used version in [Dockerfile](Dockerfile#L1-L2)).
-### 1. Create a GitHub token
+### 1. Create a GitHub personal token
From the `Developer settings` of your account settings, select `Personal access tokens` to create a new token.
@@ -446,9 +402,7 @@ No additional scopes are needed.
### 2. Install dependencies
-Connect to server and ensure [NodeJS](https://nodejs.org/en/) is installed (see tested version in [workflow](.github/workflows/workflow.yml#L18)).
-
-Run the following commands to clone this repository, install dependencies and copy configuration example file:
+Clone repository, install dependencies and copy configuration example to `settings.json`:
```shell
git clone https://github.com/lowlighter/metrics.git
@@ -457,31 +411,31 @@ npm install --only=prod
cp settings.example.json settings.json
```
-### 3. Configure your instance
+### 3. Configure your instance and start it
Edit `settings.json` to configure your instance.
```javascript
{
- //See settings.example.json for all options
//GitHub API token
- "token":"****************************************"
+ "token":"GITHUB_TOKEN",
+ //Other options...
}
```
See all supported options in [settings.example.json](settings.example.json).
-### 4. Start your instance
+If you plan to make your web instance public, it is advised to restrict its access thanks to rate limiter and access list.
-Run the following command to start your instance once you've finished configuring it:
+Once you've finished configuring metrics, start your instance:
```shell
npm start
```
-From your browser, you should be able to access your web instance on the port you provided in `setting.json`.
+Access your server with provided port in `setting.json` from your browser to ensure everything is working.
-### 5. Embed the link into your README.md
+### 4. Embed link into your README.md
Edit your repository readme and add your metrics image from your server domain:
@@ -489,14 +443,14 @@ Edit your repository readme and add your metrics image from your server domain:

```
-### 6. (optional) Setup as service on your instance
+### 6. (optional) Setup your instance as a service
To ensure that your instance will restart if it reboots or crashes, you should set it up as a service.
This is described below for Linux-like systems which support *systemd*.
Create a new service file `/etc/systemd/system/github_metrics.service` and paste the following after editing paths inside:
-```
+```ini
[Unit]
Description=Metrics
After=network-online.target
@@ -525,7 +479,7 @@ systemctl status github_metrics
โ ๏ธ HTTP errors code
-The following error codes may be encountered on a web instance:
+Following error codes may be encountered on web instance:
| Error code | Description |
| ------------------------- | -------------------------------------------------------------------------- |
@@ -541,1267 +495,218 @@ The following error codes may be encountered on a web instance:
๐ HTTP parameters
-Generated metrics images from a web instance can be configured through URL parameters.
+Most of options from [action.yml](action.yml) are actually supported by web instance, though syntax is slightly different.
+All underscores (`_`) must be replaced by dots (`.`) and `plugin_` prefixes must be dropped.
-#### Configuring base content
+For example, to configure pagespeed plugin you'd use the following:
+```
+https://my-personal-domain.com/my-github-user?pagespeed=1&pagespeed.detailed=1&pagespeed.url=https%3A%2F%2Fexample.com
+```
-Base content is enabled by default, but passing `?base=0` will disable all base content.
-You can choose to selectively enable or disable a specific `` with `?base.=<1|0>`.
+Note that url parameters must be [encoded](https://developer.mozilla.org/fr/docs/Web/JavaScript/Reference/Objets_globaux/encodeURIComponent).
-For example, to opt out from `activity`, `community` and `metadata`, append `?base.activity=0&base.community=0&base.metadata=0` to your metrics url.
+As for `base` content, which is enabled by default, sections are available through "`base.`".
-#### Configuring plugins
-
-Plugins are disabled by default, but passing `?=1` will enable a specific ``.
-Plugin options can be passed with `?.
+
+
# ๐ Documentation
-## ๐ผ๏ธ Templates
-
-Templates allow you to change the general appearance of your metrics images.
-Some metrics may be displayed differently, and all plugins may not be supported or behave the same from one template to another.
-
-Consider trying them at [metrics.lecoq.io](https://metrics.lecoq.io)!
-
-The default template is `classic`.
### ๐งฐ Template/plugin compatibily matrix
-
Template\Plugin
-
๐๏ธรP
-
โฑ๏ธ
-
๐ ร
-
๐ผ
-
๐ท๏ธ
-
๐๏ธ
-
๐ร
-
๐๏ธ
-
๐จโ๐ป
-
๐งฎ
-
๐ค
-
โ๏ธ
-
๐ก
-
๐ฐ
-
๐ร
-
โจ
-
๐ซร
-
๐งโ๐คโ๐ง
-
๐ธ
+
Template\Plugin
+
๐๏ธ
+
๐ฐ
+
๐ธ
+
๐๏ธ
+
๐ซ
+
๐ก
+
๐
+
๐ท๏ธ
+
๐จโ๐ป
+
๐ผ
+
โฑ๏ธ
+
๐งโ๐คโ๐ง
+
โ๏ธ
+
๐๏ธ
+
โจ
+
๐
+
๐
+
๐งฎ
+
๐ค
+
+
+
๐ Classic
+
โ๏ธ
+
โ๏ธ
+
โ๏ธ
+
โ๏ธ
+
โ๏ธ
+
โ๏ธ
+
โ๏ธ
+
โ๏ธ
+
โ๏ธ
+
โ๏ธ
+
โ๏ธ
+
โ๏ธ
+
โ๏ธ
+
โ๏ธ
+
โ๏ธ
+
โ๏ธ
+
โ๏ธ
+
โ๏ธ
+
โ๏ธ
-
Classic
-
โ๏ธ
-
โ๏ธ
-
โ๏ธ
-
โ๏ธN
-
โ๏ธN
-
โ๏ธ
-
โ๏ธ
-
โ๏ธ
-
โ๏ธ
-
โ๏ธ
-
โ๏ธ
-
โ๏ธ
-
โ๏ธ
-
โ๏ธ
-
โ๏ธ
-
โ๏ธ
-
โ๏ธN
-
โ๏ธN
-
โ๏ธM
+
๐ Repository
+
โ๏ธ
+
โ
+
โ
+
โ๏ธ
+
โ
+
โ
+
โ
+
โ๏ธ
+
โ๏ธ
+
โ
+
โ๏ธ
+
โ๏ธ
+
โ
+
โ๏ธ
+
โ๏ธ
+
โ
+
โ
+
โ๏ธ
+
โ
-
Terminal
-
โ๏ธP
-
โ๏ธ
-
โ
-
โ
-
โ๏ธ
-
โ๏ธ
-
โ
-
โ
-
โ๏ธ
-
โ๏ธ
-
โ
-
โ
-
โ
-
โ
-
โ
-
โ
-
โ๏ธN
-
โ
-
โ
-
-
-
RepositoryR
-
โ๏ธ
-
โ๏ธ
-
โ
-
โ
-
โ๏ธ
-
โ๏ธ
-
โ
-
โ๏ธ
-
โ๏ธ
-
โ๏ธ
-
โ
-
โ
-
โ
-
โ
-
โ
-
โ๏ธ
-
โ
-
โ๏ธM
-
โ
+
๐ Terminal
+
โ๏ธ
+
โ
+
โ
+
โ
+
โ๏ธ
+
โ
+
โ
+
โ๏ธ
+
โ๏ธ
+
โ
+
โ๏ธ
+
โ
+
โ
+
โ
+
โ
+
โ
+
โ
+
โ๏ธ
+
โ
-**Legend**
-* **P**: Partial support *(Hover cell for more informations)*
-* **M**: Feature is not released yet but is available on `@master`
-* **N**: Feature is already released, but new ones are available on `@master`
-* **R**: Repository template (all plugins content will be scoped to related repository)
-* **ร**: Feature is not supported for organization accounts
-* **รP**: Feature is supported partially for organization accounts
+## ๐ผ๏ธ Templates
-
-๐ฌ Using community templates
+Templates lets you change general appearance of rendered metrics.
+See their respective documentation for more informations about how to setup them:
- ๐ง This feature is available as pre-release on @master branch (unstable)
-
-It is possible to use official releases along with custom templates from forked repositories (not necessarily your own).
-This can be used to use different layouts, styles colors, etc.
-
-Use `setup_community_templates` option to specify additional external sources in the following format: `user/repo@branch:template`. Templates added this way will be downloaded through git and will be available with the same template name but prefixed with `@`.
-
-For example, to use the `super-metrics` template from `github-user`'s fork, add the following:
-```yaml
-- uses: lowlighter/metrics@master
- with:
- # ... other options
- template: "@super-metrics"
- setup_community_templates: github-user/metrics@master:super-metrics
-```
-
-By default, community templates have their `template.mjs` removed and fallback to the one used by `classic` template.
-It means that they're restricted to common and plugins data, to prevent malicious code injection and token leaks.
-
-If you really trust a template, it is possible to bypass this behaviour by appending `+trust` at the end of their source like below:
-```yaml
-- uses: lowlighter/metrics@master
- with:
- # ... other options
- setup_community_templates: github-user/metrics@master:super-metrics+trust
-```
-
-To create a new community template, just fork this repository and create a folder in `/source/templates` with the same structure as current templates.
-Then, it's just as simple as HTML and CSS with a bit of JavaScript!
-
-
-
-
-๐ฌ Using repository template
-
-To use `repository` template, you'll need to provide a repository name in `query` option.
-
-If the repository owner is different from the `token` owner, use the `user` option to specify it.
-
-Add the following to your workflow:
-```yaml
-- uses: lowlighter/metrics@latest
- with:
- # ... other options
- template: repository
- user: "repository-owner"
- query: '{"repo":"repository-name"}'
-```
-
-
-
-
-๐ฌ Generating metrics for organizations
-
- ๐ง This feature is available as pre-release on @master branch (unstable)
-
-It is also possible to generate metrics for organization accounts.
-
-
-
-Setup is the same as for user accounts (i.e. a personal token from an user account and use of `GITHUB_TOKEN` for commits) but you'll need to change `user` option to your organization name.
-
-Additionally, you'll need to add the `read:org` scope to your personal token, *whether you're member of target organization or not*.
-
-
-
-Resulting workflow should look like below:
-```yaml
-- uses: lowlighter/metrics@master
- with:
- # ... other options
- token: ${{ secrets.METRICS_TOKEN }} # A personal token from an user account with read:org scope
- committer_token: ${{ secrets.GITHUB_TOKEN }}
- user: "organization-name"
-```
-
-You may also need to [authorize your personal token](https://docs.github.com/en/free-pro-team@latest/github/authenticating-to-github/authorizing-a-personal-access-token-for-use-with-saml-single-sign-on) if you're using single sign-on and are encounting errors.
-
-Note that `repositories` option will be capped to 25 repositories to ensure that GraphQL queries does not timeout, so you may end up using more requests than for user accounts.
-
-Although some plugins may be noted as compatible with an organization account, it may not be actually possible to run them successfully depending of your organization size. As some of plugins use a lot of requests, you'll eventually reach the rate-limiter before all of your metrics are generated for large organizations.
-
-
+* [๐ Classic](/source/templates/classic/README.md)
+* [๐ Repository](/source/templates/repository/README.md)
+* [๐ Terminal](/source/templates/terminal/README.md)
+* [๐ Community templates](/source/templates/community/README.md)
## ๐งฉ Plugins
-Plugins are features which can provide additional metrics and features.
-In return, they may require additional configuration and consume additional API requests.
+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
+* [๐๏ธ 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)
-Generated metrics contains a few sections that are enabled by default, such as recent activity, community stats and repositories stats.
-This can be configured by explicitly opting out from them.
+### ๐ฆ Organizations metrics
+
+While metrics targets mainly user accounts, it's possible to render metrics for organization accounts.
+
+
-๐ฌ About
+๐ฌ Metrics for organizations
-By default, generated metrics contain 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
-
-You can explicitely opt out from them, which can be useful if you only want to keep a few sections or to use a plugin as standalone.
-
-For example, to keep only `header` and `repositories` sections, add the following to your workflow:
-```yaml
-- uses: lowlighter/metrics@latest
- with:
- # ... other options
- base: "header, repositories" # opt-out from "activity", "community" and "metadata"
-```
-
-#### ๐ฆ Organizations memberships
-
-By default, the `community` section only counts public organization memberships.
-You can change your membership visibility in the `People` tab of your organization:
-
-
-
-To include private organization memberships, you'll need to add the `read:org` scope to your personal token.
+Setup is the same as for user accounts, though you'll need to add `read:org` scope, **whether you're member of target organization or not**.

-You may also need to [authorize your personal token](https://docs.github.com/en/free-pro-team@latest/github/authenticating-to-github/authorizing-a-personal-access-token-for-use-with-saml-single-sign-on) if you're using single sign-on and are encounting errors.
+You'll also need to set `user` option with your organization name.
-
+If you're encounting errors and your organization is using single sign-on, try to [authorize your personal token](https://docs.github.com/en/free-pro-team@latest/github/authenticating-to-github/authorizing-a-personal-access-token-for-use-with-saml-single-sign-on).
-### โฑ๏ธ PageSpeed
+Most of plugins supported by user accounts will work with organization accounts, but note that rendering metrics for organizations consume way more APIs requests.
-The *pagespeed* plugin adds the performance statistics of the website attached on your account:
+To support private repositories, add full `repo` scope to your personal token.
-
-
-These 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).
-
-
-๐ฌ About
-
-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 429 HTTP errors.
-
-The website attached to the GitHub profile will be the one audited.
-Expect 10 to 30 seconds to generate the results.
-
-Add the following to your workflow:
-```yaml
-- uses: lowlighter/metrics@latest
- with:
- # ... other options
- plugin_pagespeed: yes
- plugin_pagespeed_token: ${{ secrets.PAGESPEED_TOKEN }}
-```
-
-You can display a detailed report along with scores:
-
-
-
-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.
-
-Add the following to your workflow instead:
-```yaml
-- uses: lowlighter/metrics@latest
- with:
- # ... other options
- plugin_pagespeed: yes
- plugin_pagespeed_detailed: yes
- plugin_pagespeed_token: ${{ secrets.PAGESPEED_TOKEN }}
-```
-
-You can also display the screenshot taken by PageSpeed API:
-
-
-
-Add the following to your workflow:
-```yaml
-- uses: lowlighter/metrics@latest
- with:
- # ... other options
- plugin_pagespeed_screenshot: yes
-```
-
-It is possible to audit a different website from the one linked to your GitHub account by using the `plugin_pagespeed_url` option.
-
-Add the following to your workflow:
-```yaml
-- uses: lowlighter/metrics@latest
- with:
- # ... other options
- plugin_pagespeed_url: https://********
-```
-
-
-
-### ๐ Isometric calendar
-
-The *isocalendar* plugin displays an isometric view of your commits calendar, along with a few stats like current streak and commit average per day.
-
-
-
-
-๐ฌ About
-
-It will consume two additional GitHub requests.
-
-Add the following to your workflow:
-```yaml
-- uses: lowlighter/metrics@latest
- with:
- # ... other options
- plugin_isocalendar: yes
-```
-
-Use the following instead to display a full-year instead:
-```yaml
-- uses: lowlighter/metrics@latest
- with:
- # ... other options
- plugin_isocalendar: yes
- plugin_isocalendar_duration: full-year
-```
-
-
-
-
-
-### ๐ผ Music
-
-The *music* plugin can work in the following modes:
-
-#### Playlist mode
-
-Select randomly a few tracks from a given playlist so you can display your favorite tracks to your visitors.
-
-
-
-
-๐ฌ About
-
-Select a music provider below for instructions.
-
-
-Apple Music
-
-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
-
-```
-
-Finish the plugin setup by adding the following to your workflow:
+#### โน๏ธ Example workflow
```yaml
- uses: lowlighter/metrics@latest
with:
# ... other options
- plugin_music: yes
- plugin_music_provider: apple
- plugin_music_mode: playlist
- plugin_music_playlist: https://******** # Extracted source link
- plugin_music_limit: 4 # Set the number of tracks you want to display
+ token: ${{ secrets.METRICS_TOKEN }} # A personal token from an user account with read:org scope
+ committer_token: ${{ secrets.GITHUB_TOKEN }} # GitHub auto-generated token
+ user: organization-name # Organization name
```
-Spotify
+๐ฌ Organizations memberships for user accounts
-Extract the *embed* URL of the playlist you want to share.
+Only public memberships can be displayed by metrics by default.
+You can manage your membership visibility in the `People` tab of your organization:
-To do so, Open Spotify and select the playlist you want to share.
-From `...` menu, select `Share` and `Copy embed code`.
+
-
+For organization memberships, add `read:org` scope to your personal token.
-Extract the source link from the code pasted in your clipboard:
-```html
-
-```
+
-Finish the plugin setup by adding the following to your workflow:
-
-```yaml
-- uses: lowlighter/metrics@latest
- with:
- # ... other options
- plugin_music: yes
- plugin_music_provider: spotify
- plugin_music_mode: playlist
- plugin_music_playlist: https://******** # Extracted source link
- plugin_music_limit: 4
-```
-
-Last.fm
+## ๐ช Customizing and contributing
-This mode is not supported for now.
+Metrics is built to be easily customizable.
+Fork this repository, switch used action from `lowlighter/metrics@latest` to your fork and start coding!
-
+To suggest a new feature, report a bug or ask for help, fill an [issue](https://github.com/lowlighter/metrics/issues) describing it.
-
+If you want to contribute, submit a [pull request](https://github.com/lowlighter/metrics/pulls).
+Be sure to read [CONTRIBUTING.md](CONTRIBUTING.md) for more information about this.
-#### Recently played mode
-
-Display tracks you have played recently.
-
-
-
-
-๐ฌ About
-
-Select a music provider below for additional instructions.
-
-
-Apple Music
-
-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.
-
-
-
-
-Spotify
-
-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":"********"
-}
-```
-
-With your `client_id`, `client_secret` and `refresh_token` you can finish the plugin setup by adding the following to your workflow:
-```yaml
-- uses: lowlighter/metrics@latest
- with:
- # ... other options
- plugin_music: yes
- plugin_music_provider: spotify
- plugin_music_token: "${{ secrets.SPOTIFY_CLIENT_ID }}, ${{ secrets.SPOTIFY_CLIENT_SECRET }}, ${{ secrets.SPOTIFY_REFRESH_TOKEN }}"
- plugin_music_mode: recent
- plugin_music_limit: 4
-```
-
-
-
-
-Last.fm
-
- ๐ง This feature is available as pre-release on @master branch (unstable)
-
-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).
-
-Finish the plugin setup by adding the following to your workflow:
-```yaml
-- uses: lowlighter/metrics@latest
- with:
- # ... other options
- plugin_music: yes
- plugin_music_provider: lastfm
- plugin_music_token: ${{ secrets.LASTFM_API_KEY }}
- plugin_music_mode: recent
- plugin_music_limit: 4
-```
-
-It is possible to use a different Last.fm username from your GitHub account by using `plugin_music_user` option.
-
-Add the following to your workflow:
-```yaml
-- uses: lowlighter/metrics@latest
- with:
- # ... other options
- plugin_music_user: ********
-```
-
-
-
-
-
-### ๐ท๏ธ Languages
-
-The *languages* plugin displays which programming languages you use the most across all your repositories.
-
-
-
-
-๐ฌ About
-
-Add the following to your workflow:
-```yaml
-- uses: lowlighter/metrics@latest
- with:
- # ... other options
- plugin_languages: yes
- plugin_languages_ignored: "" # List of comma separated languages to ignore
- plugin_languages_skipped: "" # List of comma separated repositories to skip
-```
-
- ๐ง Feature below is available as pre-release on @master branch (unstable)
-
-It is possible to use custom colors for languages if those provided by GitHub do not suit you by adding the following to your workflow:
-```yaml
-- uses: lowlighter/metrics@latest
- with:
- # ... other options
- plugin_languages: yes
- plugin_languages_colors: "0:orange, javascript:#ff0000, ..." # Make most used languages orange and JavaScript red
-```
-
-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).
-
-Use the special value `rainbow` to use rainbow colors. Use `complementary` to use [complementary colors](https://en.wikipedia.org/wiki/Complementary_colors).
-
-
-
-### ๐๏ธ Follow-up
-
-The *follow-up* 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.
-
-
-
-
-๐ฌ About
-
-Add the following to your workflow:
-```yaml
-- uses: lowlighter/metrics@latest
- with:
- # ... other options
- plugin_followup: yes
-```
-
-
-
-### ๐ 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.
-
-
-
-
-๐ฌ About
-
-This uses puppeteer to navigate through your starred topics page.
-
-You can choose to display and order topics by:
-- Most `stars`
-- Recent `activity`
-- Recently `starred` by you
-- `random`
-
-Add the following to your workflow:
-```yaml
-- uses: lowlighter/metrics@latest
- with:
- # ... other options
- plugin_topics: yes
- plugin_topics_sort: stars
- plugin_topics_limit: 15
-```
-
-It is possible to display starred topics as `Mastered and known technologies` instead:
-
-
-
-Add the following to your workflow instead:
-```yaml
-- uses: lowlighter/metrics@latest
- with:
- # ... other options
- plugin_topics: yes
- plugin_topics_mode: mastered
- plugin_topics_limit: 0
-```
-
-
-
-### ๐๏ธ Projects
-
- โ ๏ธ This plugin requires a personal token with public_repo scope.
-
-The *projects* plugin displays the progress of your profile projects.
-
-
-
-
-๐ฌ About
-
-It will consume an additional GitHub request.
-
-Because of GitHub REST API limitation, provided token requires `public_repo` scope to access projects informations.
-
-Add the following to your workflow:
-```yaml
-- uses: lowlighter/metrics@latest
- with:
- # ... other options
- plugin_projects: yes
- plugin_projects_limit: 4
-```
-
-Note that by default, profile 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).
-
-
-
-
-๐ฌ Create a personal project on GitHub
-
-On your profile, select the `Projects` tab:
-
-
-Fill the informations and set visibility to *public*:
-
-
-
-
-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.
-
-
-
-Add the following to your workflow:
-```yaml
-- uses: lowlighter/metrics@latest
- with:
- # ... other options
- plugin_projects: yes
- plugin_projects_repositories: :user/:repository1/projects/:project_id, :user/:repository2/projects/:project_id, ...
-```
-
- ๐ง This feature is available as pre-release on @master branch (unstable)
-
-It is also possible to display projects descriptions by adding the following to your workflow:
-```yaml
-- uses: lowlighter/metrics@master
- with:
- # ... other options
- plugin_projects: yes
- plugin_projects_descriptions: yes
-```
-
-
-
-### ๐จโ๐ป Lines
-
-The *lines* of code plugin displays the number of lines of code you have added and removed across all of your repositories.
-
-
-
-
-๐ฌ About
-
-It will consume an additional GitHub request per repository.
-
-Add the following to your workflow:
-```yaml
-- uses: lowlighter/metrics@latest
- with:
- # ... other options
- plugin_lines: yes
-```
-
-
-
-### ๐งฎ Traffic
-
- โ ๏ธ This plugin requires a personal token with repo scope.
-
-The repositories *traffic* plugin displays the number of page views across your repositories.
-
-
-
-
-๐ฌ About
-
-It will consume an additional GitHub request per repository.
-
-Because of GitHub REST API limitation, the provided token requires full `repo` scope to access traffic informations.
-
-
-
-Add the following to your workflow:
-```yaml
-- uses: lowlighter/metrics@latest
- with:
- # Token with "repo" scope
- token: ${{ secrets.METRICS_TOKEN }}
- # ... other options
- plugin_traffic: yes
-```
-
-
-
-### ๐ค Tweets
-
-The recent *tweets* plugin displays your latest tweets of the [Twitter](https://twitter.com) account attached to your account.
-
-
-
-
-๐ฌ About
-
-Add the following to your workflow:
-```yaml
-- uses: lowlighter/metrics@latest
- with:
- # ... other options
- plugin_tweets: yes
- plugin_tweets_token: ${{ secrets.TWITTER_TOKEN }}
-```
-
-It is possible to use a different Twitter username from the one linked to your GitHub account by using `plugin_tweets_user` option.
-
-Add the following to your workflow:
-```yaml
-- uses: lowlighter/metrics@latest
- with:
- # ... other options
- plugin_tweets_user: ********
-```
-
-
-๐ฌ Obtaining a Twitter token
-
-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.
-
-
-
-
-
-
-
-### โ๏ธ Posts
-
-The recent *posts* plugin displays recent articles you wrote on an external source, like [dev.to](https://dev.to).
-
-
-
-
-๐ฌ About
-
-Supported sources are:
-* [dev.to](https://dev.to)
-
-Add the following to your workflow:
-```yaml
-- uses: lowlighter/metrics@latest
- with:
- # ... other options
- plugin_posts: yes
- plugin_posts_source: ********
-```
-
-It is possible to use a different username from your GitHub account by using `plugin_posts_user` option.
-
-Add the following to your workflow:
-```yaml
-- uses: lowlighter/metrics@latest
- with:
- # ... other options
- plugin_posts_user: ********
-```
-
-
-
-### ๐ก Habits
-
-The coding *habits* plugin adds deduced coding habits based on your recent activity, from up to 1000 events.
-
-
-
-
-๐ฌ About
-
-It will consume an additional GitHub request per event fetched.
-
-Because of GitHub REST API limitation, provided token requires full `repo` scope to access **private** events.
-Events that cannot be fetched will be ignored so it is still possible to use this plugin with a scope-less token.
-
-A high value must be provided for `plugin_habits_from` in order for this section to be accurate, although it'll increase the number of GitHub requests sent.
-If you're using GitHub Api in other projects, you could reach the rate limit.
-
-
-
-These facts are generated from your recent coding activity.
-The indent style is deduced from the diffs of your recent commits.
-
-Add the following to your workflow:
-```yaml
-- uses: lowlighter/metrics@latest
- with:
- # ... other options
- plugin_habits: yes
- plugin_habits_from: 200
- plugin_habits_days: 14
-```
-
-You can display charts in this section:
-
-
-
-These charts are generated from your recent coding activity.
-Languages metrics are computed with [github/linguist](https://github.com/github/linguist) from the diffs of your recent commits.
-
-Add the following to your workflow instead:
-```yaml
-- uses: lowlighter/metrics@latest
- with:
- # ... other options
- plugin_habits: yes
- plugin_habits_from: 200
- plugin_habits_days: 14
- plugin_habits_facts: yes
- plugin_habits_charts: yes
-```
-
-By default, dates are based on the Greenwich meridian (GMT/UTC). In order to these metrics to be accurate, 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):
-
-```yaml
-- uses: lowlighter/metrics@latest
- with:
- # ... other options
- config_timezone: Europe/Paris
-```
-
-
-
-### ๐ฐ Activity
-
-The *activity* plugin displays your recent activity on GitHub.
-
-
-
-
-๐ฌ About
-
-It will consume an additional GitHub request.
-
-Add the following to your workflow:
-```yaml
-- uses: lowlighter/metrics@latest
- with:
- # ... other options
- plugin_activity: yes
- plugin_activity_limit: 5
- plugin_activity_days: 14 # Max age for events, set to 0 for unlimited
-```
-
-Metrics use 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 |
-
-It is possible to filter the type of events you want to display by using `plugin_activity_filter` option.
-Use the special value `"all"` (default value) to track all events.
-
-Add the following to your workflow:
-```yaml
-- uses: lowlighter/metrics@latest
- with:
- # ... other options
- plugin_activity: yes
- plugin_activity_filter: issue, pr
-```
-
-
-
-### ๐ Stars
-
-The *stars* plugin displays your recently starred repositories.
-
-
-
-
-๐ฌ About
-
-It will consume an additional GitHub request.
-
-Add the following to your workflow:
-```yaml
-- uses: lowlighter/metrics@latest
- with:
- # ... other options
- plugin_stars: yes
- plugin_stars_limit: 4
-```
-
-
-
-### โจ Stargazers
-
-The *stargazers* plugin displays your stargazers evolution across all of your repositories over the last two weeks.
-
-
-
-
-๐ฌ About
-
-It will consume additional GitHub requests per repository per set of 100 stargazers.
-
-Add the following to your workflow:
-```yaml
-- uses: lowlighter/metrics@latest
- with:
- # ... other options
- plugin_stargazers: yes
-```
-
-
-
-### ๐ซ Gists
-
-The *gists* plugin displays your [gists](https://gist.github.com) metrics.
-
-
-
-
-๐ฌ About
-
-It will consume an additional GitHub request per gist fetched.
-
-Add the following to your workflow:
-```yaml
-- uses: lowlighter/metrics@latest
- with:
- # ... other options
- plugin_gists: yes
-```
-
-
-
-### ๐งโ๐คโ๐ง People
-
-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.
-
-
-
-
-๐ฌ About
-
-It will consume an additional GitHub request per group of 100 users fetched.
-
-The following types are supported:
-
-| Type | Alias | User metrics | Repository metrics |
-| --------------- | ------------------------------------ | :----------------: | :----------------: |
-| `followers` | | โ๏ธ | โ |
-| `following` | `followed` | โ๏ธ | โ |
-| `sponsoring`* | `sponsored`, `sponsorshipsAsSponsor` | โ๏ธ | โ |
-| `sponsors`* | `sponsorshipsAsMaintainer` | โ๏ธ | โ๏ธ |
-| `contributors`* | | โ | โ๏ธ |
-| `stargazers`* | | โ | โ๏ธ |
-| `watchers`* | | โ | โ๏ธ |
-| `thanks`* | | โ๏ธ | โ๏ธ |
-
- ๐ง Types marked with * are available as pre-release on @master branch (unstable)
-
-Sections will be ordered the same as specified in `plugin_people_types`.
-`sponsors` for repositories will output the same as the owner's sponsors.
-
-Add the following to your workflow:
-```yaml
-- uses: lowlighter/metrics@latest
- with:
- # ... other options
- plugin_people: yes
- plugin_people_types: followers, following
- plugin_people_limit: 28
- plugin_people_size: 28 # Size in pixels of displayed avatars
-```
-
-It is possible to use [identicons](https://github.blog/2013-08-14-identicons/) instead of their avatar for privacy purposes.
-
-```yaml
-- uses: lowlighter/metrics@latest
- with:
- # ... other options
- plugin_people_identicons: yes
-```
-
-It is possible to thanks personally users by adding the following to your workflow:
-```yaml
-- uses: lowlighter/metrics@master
- with:
- # ... other options
- plugin_people_types: thanks
- plugin_people_thanks: github-user-1, github-user-2, ...
-```
-
-
-
-### ๐ธ Anilist
-
- ๐ง This feature is available as pre-release on @master branch (unstable)
-
-The *anilist* plugin lets you display your favorites animes, mangas and characters from [AniList](https://anilist.co) data.
-
-
-
- โน๏ธ This plugin significantly increase file size, it is advised to run it as standalone
-
-
-๐ฌ About
-
-
-
-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.
-
-Add the following to your workflow:
-```yaml
-- uses: lowlighter/metrics@master
- with:
- # ... other options
- plugin_anilist: yes
- plugin_anilist_medias: anime, manga
- plugin_anilist_sections: favorites, watching, reading, characters
- plugin_anilist_limit: 2
- plugin_anilist_shuffle: yes # Shuffle data from AniList for varied outputs
-```
-
-It is possible to use a different username from your GitHub account by using `plugin_anilist_user` option.
-
-Add the following to your workflow:
-```yaml
-- uses: lowlighter/metrics@master
- with:
- # ... other options
- plugin_anilist_user: ********
-```
-
-
-
-### ๐ง Other options
-
-A few additional options are available.
-See all supported options in [action.yml](action.yml).
-
-
-๐ฌ About
-
-#### ๐ Set timezone
-
-By default, dates are based on the Greenwich meridian (GMT/UTC).
-
-It is possible to set set your timezone (see [here](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) for a list of supported timezones) by adding the following to your workflow:
-
-```yaml
-- uses: lowlighter/metrics@latest
- with:
- # ... other options
- config_timezone: Europe/Paris
-```
-
-#### ๐ฆ Ordering content
-
-It is possible to order metrics content by adding the following to your workflow:
-```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
-```
-
-Content ordering is done through "[partials](https://github.com/lowlighter/metrics/blob/master/source/templates/classic/partials/_.json)", which are actually content chunks of generated metrics, which may vary from one template to another.
-
-It is not mandatory to specify all partials, as the rest will automatically be appended in the default order.
-
-#### ๐ฒ Adjust padding
-
-The height of the generated metrics image is computed after being rendered through an headless browser.
-As rendering can depends on used fonts and operating system, it may render as cropped or with additional blank space at the bottom.
-
-It is possible to adjust the padding by adding the following to your workflow:
-```yaml
-- uses: lowlighter/metrics@latest
- with:
- # ... other options
- config_padding: 6%
-```
-
-Both positive and negative values are accepted, but you must specify a percentage.
-
-If you specify a single value, it'll be used as for both width and height padding.
-When two values are specified separated by a comma, the first one will be used for width and the second for height.
-
-#### ๐ Using a specific branch instead of default
-
-Is is possible to commit generated metrics in a specific branch rather than default branch by adding the following to your workflow:
-```yaml
-- uses: lowlighter/metrics@latest
- with:
- # ... other options
- committer_branch: my-branch
-```
-
-#### ๐ด Including forked repositories
-
-Is is possible to include forked repositories into generated metrics by adding the following to your workflow:
-```yaml
-- uses: lowlighter/metrics@latest
- with:
- # ... other options
- repositories_forks: yes
-```
-
-#### ๐ฑ Convert output to PNG/JPEG
-
-It is possible to convert output from SVG to PNG or JPEG images by adding the following to your workflow:
-```yaml
-- uses: lowlighter/metrics@latest
- with:
- # ... other options
- config_output: png
-```
-
-Note that `png` does not support animations while `jpeg` does not support both animations and transparency.
-
-
-
-## ๐๏ธ Release schedule
-
-New features and fixes are always available first on `@master` branch which acts as development branch.
-You can use this branch if you don't mind having your metrics workflow failing from time to time.
-Fork this repository if you want to manage head commit yourself and ensure you're always on a working version of metrics.
-
-When both [Planned for next release](https://github.com/lowlighter/metrics/projects/1#column-12378679) and [In progress](https://github.com/lowlighter/metrics/projects/1#column-12158618) project columns are empty, a new version of metrics will be released soon after.
-
-`@latest` tag wil be updated to latest release of metrics, which means it doesn't required any action from your side if you're using this tag.
-New releases (even majors versions) never introduce breaking changes from an user point of view, so you can flawlessy follow release cycles without worrying.
-Hot fixes may be applied after releases without changing version number.
-
-## ๐ช Contributing and customizing
-
-To suggest a new feature, find a bug or need help, fill an [issue](https://github.com/lowlighter/metrics/issues) describing your problem or your needs.
-
-If you're motivated enough, you can submit a [pull request](https://github.com/lowlighter/metrics/pulls) to integrate new features or to solve open issues.
-
-Read [CONTRIBUTING.md](CONTRIBUTING.md) for more information about this.
## ๐ Useful references
@@ -1818,3 +723,12 @@ Read [CONTRIBUTING.md](CONTRIBUTING.md) for more information about this.
* [ankurparihar/readme-pagespeed-insights](https://github.com/ankurparihar/readme-pagespeed-insights)
* [jasonlong/isometric-contributions](https://github.com/jasonlong/isometric-contributions)
* [jamesgeorge007/github-activity-readme](https://github.com/jamesgeorge007/github-activity-readme)
+
+## ๐ License
+
+```
+MIT License
+Copyright (c) 2020 lowlighter
+```
+
+
diff --git a/action.yml b/action.yml
index 459389e9..a245cb3b 100644
--- a/action.yml
+++ b/action.yml
@@ -3,583 +3,540 @@
inputs:
- # Personal user token
+ # ====================================================================================
+ # ๐๏ธ Base content
+
+ # Base content
+ base:
+ description: Metrics base content
+ default: header, activity, community, repositories, 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
+ default: 100
+
+ # Include forked repositories into metrics
+ repositories_forks:
+ description: Include forks in metrics
+ default: no
+
+ # ====================================================================================
+ # ๐งฑ Core
+
+ # 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
required: true
-
+
+ # GitHub username
+ user:
+ description: GitHub username
+ default: ""
+
# Set to "${{ secrets.GITHUB_TOKEN }}"
committer_token:
description: GitHub Token used to commit metrics
default: ""
-
- # Branch to commit
+
+ # Branch used to commit rendered metrics
committer_branch:
- description: The branch used to commit metrics
+ description: Branch used to commit rendered metrics
default: ""
-
- # GitHub username
- # Optional, as it defaults "token"'s owner
- user:
- description: GitHub username
- default: ""
-
- # Output path for generated metrics, relative to repository's root
+
+ # Rendered metrics output path, relative to repository's root
filename:
- description: Path of SVG image output
+ description: Rendered metrics output path
default: github-metrics.svg
-
- # Optimize SVG image with SVGO
- # It minifies and removes useless attributes
+
+ # Optimize SVG image to reduce its filesize
# Some templates may not support this option
optimize:
description: SVG optimization
default: yes
-
- # Setup additional templates from remote repositories (like forks)
- # Format is : user/repo@branch:template
- # To use a community template, set "template" option to "@template" (where template is the template name)
+
+ # Setup additional templates from remote repositories
setup_community_templates:
description: Additional community templates to setup
default: ""
-
- # Timezone used by metrics
- # See https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
- # Some plugins will use it to calibrate dates
- config_timezone:
- description: Timezone used
- default: ""
-
- # Metrics output type
- # Supported values are :
- # - svg
- # - png (does not support animations)
- # - jpeg (does not support animations and transparency)
- config_output:
- description: Output image type
- default: svg
-
- # Enable or disable SVG CSS animations
- config_animations:
- description: SVG CSS animations
- default: yes
-
- # Configure padding for output image (percentage)
- # It can be used to add padding to generated metrics if rendering is cropped or has too much empty space
- # You can specify one value (for both width and height) and two values (one for width and one for height, seperated by a comma)
- config_padding:
- description: Image padding
- default: 6%
-
- # Configure metrics content order (comma-separated values)
- # Specify in which order base and plugins will be displayed
- # It is not mandatory to specify all partials when using this option, in this case, remaining parts will be appended
- #
- # For example, to display "base.repositories" before "base.activity" and "base.community" in "classic template" you can use:
- # config_order: base.header, base.repositories, base.activity+community
- #
- # See source/templates/*/partials/_.json for a list of supported partials for each template.
- config_order:
- description: Configure metrics content order
- default: ""
-
- # Number of repositories to use for metrics
- # A high number increase metrics accuracy, but will consume additional API requests when using plugins
- repositories:
- description: Number of repositories to use
- default: 100
-
- # Whether to include forked repositories into metrics
- repositories_forks:
- description: Include forks in metrics
- default: no
-
+
# Template to use
- # See https://github.com/lowlighter/metrics/tree/master/source/templates for supported templates
+ # To use community template, prefix its name with "@"
template:
description: Template to use
default: classic
-
- # Raw query parameters (JSON string)
+
+ # 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
default: "{}"
-
- # Template base content
- # Pass a string of comma-separated values
- # To disable everything (like if you want to use a plugin as standalone), pass an empty string
- # Supported values are :
- # - "header" (name, commits calendar, ...)
- # - "activity" (commits, issues/pull requests opened, ...)
- # - "community" (following, stars, sponsors, ...)
- # - "repositories" (license, stars, forks, ...)
- # - "metadata" (svg generation metadata)
- base:
- description: Base content to include in metrics
- default: "header, activity, community, repositories, metadata"
-
- # Google PageSpeed plugin
- # Enable it to compute the performance of provided website
- plugin_pagespeed:
- description: Enable Google PageSpeed metrics for user's website
- default: no
-
- # Website to audit with PageSpeed
- # Leave empty to default to the website attached to "user"'s GitHub account
- plugin_pagespeed_url:
- description: Website to audit with PageSpeed
+
+ # Timezone used by metrics
+ # See https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
+ config_timezone:
+ description: Timezone used
default: ""
-
- # Display additional PageSpeed audit metrics
- # The following are displayed :
- # 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: Display additional PageSpeed metrics
- default: no
-
- # Display the final screenshot of audited website taken by PageSpeed audit
- plugin_pagespeed_screenshot:
- description: Display a screenshot of your website
- default: no
-
- # PageSpeed API token (optional, avoid hitting requests limit)
- # See https://developers.google.com/speed/docs/insights/v5/get-started for more informations
- plugin_pagespeed_token:
- description: Pagespeed personal token
+
+ # 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
default: ""
-
- # Lines of code plugin
- # Compute added/removed line for each of you repositories from your contributors stats
- plugin_lines:
- description: Enable lines of code metrics
- default: no
-
- # Traffic plugin ("token" must have "repo" permission)
- # Count views on your repositories
- plugin_traffic:
- description: Enable repositories traffic metrics
- default: no
-
- # Coding habits plugin
- # Search in your recent activity what've recently did and deduce facts/charts
- plugin_habits:
- description: Enable coding habits metrics
- default: no
-
- # Number of activity events to base habits on
- # Capped to 1000
- plugin_habits_from:
- description: Number of activity events to use
- default: 200
-
- # Number of days to base habits on (older events will be discarded)
- # Capped to 30
- plugin_habits_days:
- description: Number of days to use
- default: 14
-
- # Display tidbits about your active hours/days, indent used (spaces/tabs), etc. deduced from recent activity
- plugin_habits_facts:
- description: Display habits facts based on recent activity
+
+ # Enable SVG CSS animations
+ config_animations:
+ description: SVG CSS animations
default: yes
-
- # Display charts of most active time of the day and languages recently used
- plugin_habits_charts:
- description: Display recent coding activity charts
- default: no
-
- # Languages plugins
- # Compute the most used programming languages on your repositories
- plugin_languages:
- description: Enable most used languages metrics
- default: no
-
- # List of ignored languages, comma separated
- # Ignored languages won't count towards your languages metrics
- plugin_languages_ignored:
- description: List of ignored languages
- default: ""
-
- # List of skipped repositories, comma separated
- # Skipped repositories won't count towards your languages metrics
- plugin_languages_skipped:
- description: List of skipped repositories
- default: ""
-
- # List of custom colors for specified languages
- # Use this option when GitHub's color are too similar for your most used languages
- #
- # It is possible to use either hexadecimal colors or named colors
- # - Use "rainbow" values for a predefined set of colors
- # - 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)
- plugin_languages_colors:
- description: Custom languages colors
- default: ""
-
- # Follow-up plugin
- # Display the number and the ratio of opened/closed issues and opened/merged pull requests on your repositories
- plugin_followup:
- description: Enable owned repositories issues and pull requests metrics
- default: no
-
- # Music plugin
- # Display tracks you recently listened or your favorite tracks from a playlist
- plugin_music:
- description: Enable music plugin
- default: no
-
- # Music provider
- # Required in "recent" mode
- # Optional in "playlist" mode (will be deduced from "plugin_music_playlist" url)
- # Supported values are :
- # - "apple" for Apple Music
- # - "spotify" for Spotify
- # - "lastfm" for Last.fm
- plugin_music_provider:
- description: Name of the music provider you're using
- default: ""
-
- # Music personal token
- # This may be required depending on the music provider and the mode you use
- # - "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
- default: ""
-
- # Music plugin mode
- # Supported values are :
- # - "playlist" : display tracks from a playlist randomly
- # - "recent" : display recently played tracks
- plugin_music_mode:
- description: Use "recent" to display recently played music and "playlist" to display tracks randomly from a given playlist
- default: ""
-
- # Music playlist
- # The embed playlist url (source which is used for music player iframes)
- # Will default "plugin_music_mode" to "playlist" when set
- plugin_music_playlist:
- description: Embed playlist url
- default: ""
-
- # Number of tracks to display for music plugin
- plugin_music_limit:
- description: Number of tracks to display
- default: 4
-
- # Music service username
- # Leave empty to default to the login "user"'s GitHub account
- plugin_music_user:
- description: "Music provider username"
- default: ""
-
- # Posts plugin
- # Display recent posts from an external source
- plugin_posts:
- description: Enable recent posts display
- default: no
-
- # Posts source
- # This is required when "plugin_posts" is enabled
- # Supported values are :
- # - "dev.to" for dev.to
- plugin_posts_source:
- description: Posts external source
- default: ""
-
- # Posts source username
- # Leave empty to default to the login "user"'s GitHub account
- plugin_posts_user:
- description: Posts external source username
- default: ""
-
- # Number of posts to display
- plugin_posts_limit:
- description: Number of posts to display
- default: 4
-
- # Isometric calendar plugin
- # Display an isometric view of your commits calendar along with a few stats like current streak and average commits per day
- plugin_isocalendar:
- description: Display an isometric view of your commits calendar along with a few additional stats
- default: no
-
- # Duration shown by isometric calendar plugin
- # Supported values are "half-year" and "full-year"
- plugin_isocalendar_duration:
- description: Set isometric calendar duration
- default: half-year
-
- # Gists plugin
- # Display gists metrics
- plugin_gists:
- description: Display gists metrics
- default: no
-
- # Topics plugin
- # Display starred topics
- plugin_topics:
- description: Display starred topics
- default: no
-
- # Topics plugin mode
- # Change the way topics are displayed
- # Supported values are :
- # - "starred" to display starred topics as interests labels
- # - "mastered" to display starred topics as mastered/known technologies icons
- plugin_topics_mode:
- description: Display starred topics
- default: "starred"
-
- # Sorting method of displayed topics
- # Supported values are :
- # - "stars" to sort them from most starred to least starred
- # - "activity" to sort them from most recent activity to least recent activity
- # - "starred" to sort them from your most recently starred to your least recently starred
- # - "random" to sort them randomly
- plugin_topics_sort:
- description: Sorting method of starred topics
- default: "stars"
-
- # Number of topics to display (between 0 and 20)
- # If more topics must be displayed, they will be grouped in an ellipsis
- plugin_topics_limit:
- description: Number of starred topics to display
- default: ""
-
- # Projects plugin
- # Display active projects
- plugin_projects:
- description: Display active projects
- default: no
-
- # Display active repository projects
- # By default, only user owned projects are displayed, with this option it is possible to display projects from repositories
- # List of comma-separated projects identifier, in the following format :user/:repo/projects/:project_id
- # The limit of displayed projects will automatically be updated so listed projects will fit
- plugin_projects_repositories:
- description: List of repository project identifiers to disaplay
- default: ""
-
- # Number of active projects to display
- # Between 1 and 100
- plugin_projects_limit:
- description: Number of active projects to display
- default: 4
-
- # Display projects descriptions if enabled
- plugin_projects_descriptions:
- description: Display projects descriptions
- default: no
-
- # Tweets plugin
- # Enable it to display recent tweets of the twitter username attached to "user"
- plugin_tweets:
- description: Display recent tweets
- default: no
-
- # Twitter username
- # Leave empty to default to the twitter account attached to "user"'s GitHub account
- plugin_tweets_user:
- description: Twitter username
- default: ""
-
- # Tweets API token (required when tweets plugin is enabled)
- # See https://apps.twitter.com for more informations
- plugin_tweets_token:
- description: Twitter bearer token
- default: ""
-
- # Number of tweets to display
- # Between 1 and 10
- plugin_tweets_limit:
- description: Number of tweets to display
- default: 2
-
- # Display recently starred repositories
- plugin_stars:
- description: Display recently starred repositories
- default: no
-
- # Number of recently starred repositories to display
- plugin_stars_limit:
- description: Number of recently starred repositories to display
- default: 4
-
- # Display stargazers evolution over the last two weeks
- # It shows total stargazers along with increase rate per day
- plugin_stargazers:
- description: Display stargazers evolution over the last two weeks
- default: no
-
- # Display recent activity
- plugin_activity:
- description: Display recent activity
- default: no
-
- # Number of activity events to display
- # Capped to 100
- plugin_activity_limit:
- description: Number of activity events to display
- default: 5
-
- # Discard older events
- # Use 0 to display activity whatever the date
- plugin_activity_days:
- description: Maximum activity event age
- default: 14
-
- # Events type to display
- # Pass a string of comma-separated values
- # Supported values are
- # - "comment" for all kind of comments (commits, issue and pr)
- # - "ref/create" and "ref/delete" for tag and branch creation/deletion
- # - "release" for new published releases
- # - "wiki" for wiki edition
- # - "push" for pushed commits
- # - "issue" and "pr" for issues and pull requests
- # - "review" for pull requests review
- # - "public" for repositories made public
- # - "fork" and "star" for forked and starred repositories
- # - "member" for accepted repository invitations
- plugin_activity_filter:
- description: Events to display
- default: all
-
- # Display followed and following users
- plugin_people:
- description: Display
- default: no
-
- # Limit the number of users displayed
- plugin_people_limit:
- description: Number of users to display per categorie
- default: 28
-
- # Configure image size of users' avatar
- plugin_people_size:
- description: Size of users' avatars
- default: 28
-
- # List of users categories to display (comma separated)
- # For user's metrics, supported values are:
- # - "followers"
- # - "following"/"followed"
- # - "sponsors"/"sponsorshipsAsMaintainer"
- # - "sponsoring"/"sponsored"/"sponsorshipsAsSponsor"
- # - "thanks" (see "plugin_people_thanks" below)
- # For repositories' metrics, supported values are:
- # - "contributors"
- # - "stargazers"
- # - "watchers"
- # - "sponsors"/"sponsorshipsAsMaintainer"
- # - "thanks" (see "plugin_people_thanks" below)
- plugin_people_types:
- description: Categories to display
- default: followers, following
-
- # List of users to thanks (comma seperated)
- # When using "thanks" as a type, it'll display the users you listed in this option
- # This can be used to create "Special thanks" badges that you can embed elsewhere
- plugin_people_thanks:
- description: Users to thanks in "thanks" section type
- default: ""
-
- # Display GitHub identicons instead of users' real avatar
- # Mostly for privacy purposes
- plugin_people_identicons:
- description: Use identicons instead of real avatars
- default: no
-
- # Display your favorites animes and mangas from AniList
- plugin_anilist:
- description: Display your favorites animes and mangas from AniList
- default: no
-
- # Medias to display from AniList (comma-separated list)
- # Supported values are:
- # - "anime"
- # - "manga"
- plugin_anilist_medias:
- description: Medias to display from AniList data
- default: anime, manga
-
- # Sections to display from AniList data (comma-separated list)
- # Values in "plugin_anilist_medias" may also impact displayed sections
- # Supported values are:
- # - "favorites" for favorites animes/mangas
- # - "watching" for currently watched animes
- # - "reading" for currently read mangas
- # - "characters" for favorites characters
- plugin_anilist_sections:
- description: Sections to display from AniList data
- default: favorites
-
- # Maximum number of medias to display per section from AniList Data
- plugin_anilist_limit:
- description: Medias to display
- default: 2
-
- # Shuffle AniList data
- plugin_anilist_shuffle:
- description: Shuffle AniList data
- default: yes
-
- # Username on AniList
- # Default to GitHub username
- plugin_anilist_user:
- description: AniList login
- default: ""
-
- # ====================================================================================
- # Options below are mostly used for testing
-
- # When enabled, any plugins errors will throw
- # By default, metrics are still generated with an error message
+
+ # 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
+ default: 6%
+
+ # Metrics output format
+ config_output:
+ description: Output image format
+ default: svg
+
+ # 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
default: no
-
- # Enable debug mode
+
+ # Debug mode
+ # Note that this will automatically be enabled if job fails
debug:
- description: Enable debug logs
+ description: Debug logs
default: no
-
- # Verify SVG after generation
- # Test whether SVG can be correctly parsed
+
+ # Ensure SVG can be correctly parsed after generation
verify:
- description: Verify SVG after generation
+ description: Verify SVG
default: no
-
+
# Debug flags
debug_flags:
description: Debug flags
default: ""
-
- # Enable dry-run mode
- # Generate image but does not push it
+
+ # Dry-run mode (perform generation without pushing it)
dryrun:
description: Enable dry-run
default: no
-
- # Use mocked data
- # Bypass external APIs which requires a token and sent mocked data
+
+ # Use mocked data to bypass external APIs
use_mocked_data:
- description: Use mocked data instead of real APIs
+ description: Use mocked data instead of live APIs
default: no
-
- # Use pre-built image from GitHub registry (experimental)
+
+ # 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
default: ""
+
+ # ====================================================================================
+ # ๐ฐ Recent activity
+
+ # Enable or disable plugin
+ plugin_activity:
+ description: Display recent activity
+ default: no
+
+ # Number of activity events to display
+ plugin_activity_limit:
+ description: Maximum number of events to display
+ default: 5
+
+ # Filter events by age
+ # Set to 0 to disable age filtering
+ plugin_activity_days:
+ description: Maximum event age
+ default: 14
+
+ # Filter events by type
+ plugin_activity_filter:
+ description: Events types to keep
+ default: all
+
+ # ====================================================================================
+ # ๐ธ Anilist
+
+ # Enable or disable plugin
+ plugin_anilist:
+ description: Display data from your AniList account
+ default: no
+
+ # Types of medias to display
+ plugin_anilist_medias:
+ description: Medias types to display
+ default: anime, manga
+
+ # Sections to display
+ # Values from "plugin_anilist_medias" may impact displayed sections
+ plugin_anilist_sections:
+ description: Sections to display
+ default: favorites
+
+ # 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
+ default: 2
+
+ # Shuffle AniList data for varied outputs
+ plugin_anilist_shuffle:
+ description: Shuffle AniList data
+ default: yes
+
+ # Username on AniList
+ plugin_anilist_user:
+ description: AniList login
+ default: .user.login
+
+ # ====================================================================================
+ # ๐๏ธ Follow-up of issues and pull requests
+
+ # Enable or disable plugin
+ plugin_followup:
+ description: Display follow-up of repositories issues and pull requests
+ default: no
+
+ # ====================================================================================
+ # ๐ซ Gists
+
+ # Enable or disable plugin
+ plugin_gists:
+ description: Display gists metrics
+ default: no
+
+ # ====================================================================================
+ # ๐ก Coding habits
+
+ # Enable or disable plugin
+ plugin_habits:
+ description: Display coding habits metrics
+ 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
+ default: 200
+
+ # Filter used events to compute habits by age
+ plugin_habits_days:
+ description: Maximum event age
+ default: 14
+
+ # 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
+ 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
+ default: no
+
+ # ====================================================================================
+ # ๐ Isometric commit calendar
+
+ # Enable or disable plugin
+ plugin_isocalendar:
+ description: Display an isometric view of your commits calendar
+ default: no
+
+ # Set time window shown by isometric calendar
+ plugin_isocalendar_duration:
+ description: Set time window shown by isometric calendar
+ default: half-year
+
+ # ====================================================================================
+ # ๐ท๏ธ Most used languages
+
+ # Enable or disable plugin
+ plugin_languages:
+ description: Display most used languages metrics
+ default: no
+
+ # List of languages that will be ignored
+ plugin_languages_ignored:
+ description: Languages to ignore
+ default: ""
+
+ # List of repositories that will be skipped
+ plugin_languages_skipped:
+ description: Repositories to skip
+ 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
+ default: github
+
+ # ====================================================================================
+ # ๐จโ๐ป Lines of code changed
+
+ # Enable or disable plugin
+ plugin_lines:
+ description: Display lines of code metrics
+ default: no
+
+ # ====================================================================================
+ # ๐ผ Music plugin
+
+ # Enable or disable plugin
+ plugin_music:
+ description: Display your music tracks
+ 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
+ default: ""
+
+ # 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
+ default: ""
+
+ # Plugin mode
+ plugin_music_mode:
+ description: Plugin mode
+ default: ""
+
+ # Embed playlist url (i.e. url used by music player iframes)
+ plugin_music_playlist:
+ description: Embed playlist url
+ default: ""
+
+ # Number of music tracks to display
+ plugin_music_limit:
+ description: Maximum number of tracks to display
+ default: 4
+
+ # Username on music provider service
+ plugin_music_user:
+ description: Music provider username
+ default: .user.login
+
+ # ====================================================================================
+ # โฑ๏ธ Website performances
+
+ # Enable or disable plugin
+ plugin_pagespeed:
+ description: Display a website Google PageSpeed metrics
+ default: no
+
+ # Website to audit with PageSpeed
+ plugin_pagespeed_url:
+ description: Audited website
+ 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
+ default: no
+
+ # Display a screenshot of audited website
+ # May increases significantly filesize
+ plugin_pagespeed_screenshot:
+ description: Display a screenshot of your website
+ 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
+ default: ""
+
+ # ====================================================================================
+ # ๐งโ๐คโ๐ง People plugin
+
+ # Enable or disable plugin
+ plugin_people:
+ description: Display GitHub users from various affiliations
+ default: no
+
+ # Number of users to display per section
+ plugin_people_limit:
+ description: Maximum number of user to display
+ default: 28
+
+ # Size of displayed user's avatar
+ plugin_people_size:
+ description: Size of displayed GitHub users' avatars
+ default: 28
+
+ # List of section to display
+ # Ordering will be kept
+ plugin_people_types:
+ description: Affiliations to display
+ default: followers, following
+
+ # 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
+ default: ""
+
+ # Use GitHub identicons instead of users' avatar (for privacy purposes)
+ plugin_people_identicons:
+ description: Use identicons instead of avatars
+ default: no
+
+ # ====================================================================================
+ # โ๏ธ Recent posts
+
+ # Enable or disable plugin
+ plugin_posts:
+ description: Display recent posts
+ default: no
+
+ # Posts external source
+ plugin_posts_source:
+ description: Posts external source
+ default: ""
+
+ # Number of posts to display
+ plugin_posts_limit:
+ description: Maximum number of posts to display
+ default: 4
+
+ # Username on external posts source
+ plugin_posts_user:
+ description: Posts external source username
+ default: .user.login
+
+ # ====================================================================================
+ # ๐๏ธ Projects
+
+ # Enable or disable plugin
+ plugin_projects:
+ description: Display active projects
+ 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
+ default: 4
+
+ # 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
+ default: ""
+
+ # Display projects descriptions
+ plugin_projects_descriptions:
+ description: Display projects descriptions
+ default: no
+
+ # ====================================================================================
+ # โจ Stargazers over last weeks
+
+ # Enable or disable plugin
+ plugin_stargazers:
+ description: Display stargazers metrics
+ default: no
+
+ # ====================================================================================
+ # ๐ Recently starred repositories
+
+ # Enable or disable plugin
+ plugin_stars:
+ description: Display recently starred repositories
+ default: no
+
+ # Number of stars to display
+ plugin_stars_limit:
+ description: Maximum number of stars to display
+ default: 4
+
+ # ====================================================================================
+ # ๐ Starred topics
+
+ # Enable or disable plugin
+ plugin_topics:
+ description: Display starred topics
+ default: no
+
+ # Plugin mode
+ plugin_topics_mode:
+ description: Plugin mode
+ default: starred
+
+ # Topics sorting order
+ plugin_topics_sort:
+ description: Sorting method of starred topics
+ default: stars
+
+ # 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
+ default: 15
+
+ # ====================================================================================
+ # ๐งฎ Repositories traffic
+
+ # Enable or disable plugin
+ plugin_traffic:
+ description: Display repositories traffic metrics
+ default: no
+
+ # ====================================================================================
+ # ๐ค Latest tweets
+
+ # Enable or disable plugin
+ plugin_tweets:
+ description: Display recent tweets
+ default: no
+
+ # Twitter API token
+ # See https://apps.twitter.com for more informations
+ plugin_tweets_token:
+ description: Twitter API token
+ default: ""
+
+ # Number of tweets to display
+ plugin_tweets_limit:
+ description: Maximum number of tweets to display
+ default: 2
+
+ # Twitter username
+ plugin_tweets_user:
+ description: Twitter username
+ default: .user.twitter
+
# ====================================================================================
# Action metadata
@@ -627,6 +584,7 @@ runs:
if [[ $METRICS_USE_PREBUILT_IMAGE ]]; then
echo "Using pre-built version $METRICS_TAG, will pull docker image from GitHub registry"
METRICS_IMAGE=ghcr.io/lowlighter/metrics:$METRICS_TAG
+ docker image pull $METRICS_IMAGE > /dev/null
# Official action
elif [[ $METRICS_SOURCE == "lowlighter" ]]; then
# Is unreleased version
@@ -640,18 +598,28 @@ runs:
METRICS_IMAGE=ghcr.io/lowlighter/metrics:$METRICS_TAG
# Use registry for released version
else
- echo "Using an unreleased version, rebuilding docker image from Dockerfile"
- docker build -t metrics:unreleased .
- METRICS_IMAGE=metrics:unreleased
+ echo "Using an unreleased version ($METRICS_VERSION)"
+ METRICS_IMAGE=metrics:$METRICS_VERSION
fi
# Forked action
else
- echo "Using a forked version, rebuilding docker image from Dockerfile"
- docker build -t metrics:forked .
- METRICS_IMAGE=metrics:forked
+ echo "Using a forked version"
+ METRICS_IMAGE=metrics:forked-$METRICS_VERSION
fi
echo "Image name: $METRICS_IMAGE"
+ # Build image if necessary
+ set +e
+ docker image inspect $METRICS_IMAGE > /dev/null
+ METRICS_IMAGE_NEEDS_BUILD="$?"
+ set -e
+ if [[ "$METRICS_IMAGE_NEEDS_BUILD" -gt "0" ]]; then
+ echo "Image $METRICS_IMAGE is not present locally, rebuilding it from Dockerfile"
+ docker build -t $METRICS_IMAGE . > /dev/null
+ else
+ echo "Image $METRICS_IMAGE is present locally"
+ fi
+
# Run docker image with current environment
docker run --volume $GITHUB_EVENT_PATH:$GITHUB_EVENT_PATH --env-file .env $METRICS_IMAGE
rm .env
diff --git a/package-lock.json b/package-lock.json
index e9679d02..c0ce351a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -894,18 +894,46 @@
}
}
},
- "@octokit/auth-token": {
- "version": "2.4.4",
- "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-2.4.4.tgz",
- "integrity": "sha512-LNfGu3Ro9uFAYh10MUZVaT7X2CnNm2C8IDQmabx+3DygYIQjs9FwzFAHN/0t6mu5HEPhxcb1XOuxdpY82vCg2Q==",
+ "@kwsites/file-exists": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz",
+ "integrity": "sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==",
"requires": {
- "@octokit/types": "^6.0.0"
+ "debug": "^4.1.1"
+ },
+ "dependencies": {
+ "debug": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz",
+ "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==",
+ "requires": {
+ "ms": "2.1.2"
+ }
+ },
+ "ms": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
+ }
+ }
+ },
+ "@kwsites/promise-deferred": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@kwsites/promise-deferred/-/promise-deferred-1.1.1.tgz",
+ "integrity": "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw=="
+ },
+ "@octokit/auth-token": {
+ "version": "2.4.5",
+ "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-2.4.5.tgz",
+ "integrity": "sha512-BpGYsPgJt05M7/L/5FoE1PiAbdxXFZkX/3kDYcsvd1v6UhlnE5e96dTDr0ezX/EFwciQxf3cNV0loipsURU+WA==",
+ "requires": {
+ "@octokit/types": "^6.0.3"
}
},
"@octokit/core": {
- "version": "3.2.4",
- "resolved": "https://registry.npmjs.org/@octokit/core/-/core-3.2.4.tgz",
- "integrity": "sha512-d9dTsqdePBqOn7aGkyRFe7pQpCXdibSJ5SFnrTr0axevObZrpz3qkWm7t/NjYv5a66z6vhfteriaq4FRz3e0Qg==",
+ "version": "3.2.5",
+ "resolved": "https://registry.npmjs.org/@octokit/core/-/core-3.2.5.tgz",
+ "integrity": "sha512-+DCtPykGnvXKWWQI0E1XD+CCeWSBhB6kwItXqfFmNBlIlhczuDPbg+P6BtLnVBaRJDAjv+1mrUJuRsFSjktopg==",
"requires": {
"@octokit/auth-token": "^2.4.4",
"@octokit/graphql": "^4.5.8",
@@ -916,56 +944,56 @@
}
},
"@octokit/endpoint": {
- "version": "6.0.10",
- "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-6.0.10.tgz",
- "integrity": "sha512-9+Xef8nT7OKZglfkOMm7IL6VwxXUQyR7DUSU0LH/F7VNqs8vyd7es5pTfz9E7DwUIx7R3pGscxu1EBhYljyu7Q==",
+ "version": "6.0.11",
+ "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-6.0.11.tgz",
+ "integrity": "sha512-fUIPpx+pZyoLW4GCs3yMnlj2LfoXTWDUVPTC4V3MUEKZm48W+XYpeWSZCv+vYF1ZABUm2CqnDVf1sFtIYrj7KQ==",
"requires": {
- "@octokit/types": "^6.0.0",
+ "@octokit/types": "^6.0.3",
"is-plain-object": "^5.0.0",
"universal-user-agent": "^6.0.0"
}
},
"@octokit/graphql": {
- "version": "4.5.8",
- "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-4.5.8.tgz",
- "integrity": "sha512-WnCtNXWOrupfPJgXe+vSmprZJUr0VIu14G58PMlkWGj3cH+KLZEfKMmbUQ6C3Wwx6fdhzVW1CD5RTnBdUHxhhA==",
+ "version": "4.5.9",
+ "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-4.5.9.tgz",
+ "integrity": "sha512-c+0yofIugUNqo+ktrLaBlWSbjSq/UF8ChAyxQzbD3X74k1vAuyLKdDJmPwVExUFSp6+U1FzWe+3OkeRsIqV0vg==",
"requires": {
"@octokit/request": "^5.3.0",
- "@octokit/types": "^6.0.0",
+ "@octokit/types": "^6.0.3",
"universal-user-agent": "^6.0.0"
}
},
"@octokit/openapi-types": {
- "version": "2.3.1",
- "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-2.3.1.tgz",
- "integrity": "sha512-KTzpRDT07euvbBYbPs121YDqq5DT94nBDFIyogsDhOnWL8yDCHev6myeiPTgS+VLmyUbdNCYu6L/gVj+Bd1q8Q=="
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-3.3.0.tgz",
+ "integrity": "sha512-s3dd32gagPmKaSLNJ9aPNok7U+tl69YLESf6DgQz5Ml/iipPZtif3GLvWpNXoA6qspFm1LFUZX+C3SqWX/Y/TQ=="
},
"@octokit/plugin-paginate-rest": {
- "version": "2.7.1",
- "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.7.1.tgz",
- "integrity": "sha512-dUsxsEIrBqhlQNfXRhMhXOTQi0SSG38+QWcPGO226HFPFJk44vWukegHfMG3496vLv9T2oT7IuAGssGpcUg5bQ==",
+ "version": "2.9.0",
+ "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.9.0.tgz",
+ "integrity": "sha512-XxbOg45r2n/2QpU6hnGDxQNDRrJ7gjYpMXeDbUCigWTHECmjoyFLizkFO2jMEtidMkfiELn7AF8GBAJ/cbPTnA==",
"requires": {
- "@octokit/types": "^6.3.1"
+ "@octokit/types": "^6.6.0"
}
},
"@octokit/plugin-request-log": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-1.0.2.tgz",
- "integrity": "sha512-oTJSNAmBqyDR41uSMunLQKMX0jmEXbwD1fpz8FG27lScV3RhtGfBa1/BBLym+PxcC16IBlF7KH9vP1BUYxA+Eg=="
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-1.0.3.tgz",
+ "integrity": "sha512-4RFU4li238jMJAzLgAwkBAw+4Loile5haQMQr+uhFq27BmyJXcXSKvoQKqh0agsZEiUlW6iSv3FAgvmGkur7OQ=="
},
"@octokit/plugin-rest-endpoint-methods": {
- "version": "4.5.2",
- "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-4.5.2.tgz",
- "integrity": "sha512-JXoDIh+QnzFb6C5ZqIcUzDkn1fLrxawi98ZbvYb9s7Z2CJLITUWpbTAxSgseczEho18pYhamEBRR/h3o3HIXJQ==",
+ "version": "4.9.0",
+ "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-4.9.0.tgz",
+ "integrity": "sha512-EAr2epvY8JfXSi/cdMsyyfBctdKkolDH7xSgu3MKBqPRm0WfQ2QvI050jz61XZXoVK3ZgrhdMCyd1GgOFz7CSw==",
"requires": {
- "@octokit/types": "^6.3.2",
+ "@octokit/types": "^6.6.0",
"deprecation": "^2.3.1"
}
},
"@octokit/request": {
- "version": "5.4.12",
- "resolved": "https://registry.npmjs.org/@octokit/request/-/request-5.4.12.tgz",
- "integrity": "sha512-MvWYdxengUWTGFpfpefBBpVmmEYfkwMoxonIB3sUGp5rhdgwjXL1ejo6JbgzG/QD9B/NYt/9cJX1pxXeSIUCkg==",
+ "version": "5.4.13",
+ "resolved": "https://registry.npmjs.org/@octokit/request/-/request-5.4.13.tgz",
+ "integrity": "sha512-WcNRH5XPPtg7i1g9Da5U9dvZ6YbTffw9BN2rVezYiE7couoSyaRsw0e+Tl8uk1fArHE7Dn14U7YqUDy59WaqEw==",
"requires": {
"@octokit/endpoint": "^6.0.1",
"@octokit/request-error": "^2.0.0",
@@ -978,43 +1006,43 @@
}
},
"@octokit/request-error": {
- "version": "2.0.4",
- "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-2.0.4.tgz",
- "integrity": "sha512-LjkSiTbsxIErBiRh5wSZvpZqT4t0/c9+4dOe0PII+6jXR+oj/h66s7E4a/MghV7iT8W9ffoQ5Skoxzs96+gBPA==",
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-2.0.5.tgz",
+ "integrity": "sha512-T/2wcCFyM7SkXzNoyVNWjyVlUwBvW3igM3Btr/eKYiPmucXTtkxt2RBsf6gn3LTzaLSLTQtNmvg+dGsOxQrjZg==",
"requires": {
- "@octokit/types": "^6.0.0",
+ "@octokit/types": "^6.0.3",
"deprecation": "^2.0.0",
"once": "^1.4.0"
}
},
"@octokit/rest": {
- "version": "18.0.12",
- "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-18.0.12.tgz",
- "integrity": "sha512-hNRCZfKPpeaIjOVuNJzkEL6zacfZlBPV8vw8ReNeyUkVvbuCvvrrx8K8Gw2eyHHsmd4dPlAxIXIZ9oHhJfkJpw==",
+ "version": "18.0.15",
+ "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-18.0.15.tgz",
+ "integrity": "sha512-MBlZl0KeuvFMJ3210hG5xhh/jtYmMDLd5WmO49Wg4Rfg0odeivntWAyq3KofJDP2G8jskCaaOaZBKo0TeO9tFA==",
"requires": {
"@octokit/core": "^3.2.3",
"@octokit/plugin-paginate-rest": "^2.6.2",
"@octokit/plugin-request-log": "^1.0.2",
- "@octokit/plugin-rest-endpoint-methods": "4.4.1"
+ "@octokit/plugin-rest-endpoint-methods": "4.8.0"
},
"dependencies": {
"@octokit/plugin-rest-endpoint-methods": {
- "version": "4.4.1",
- "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-4.4.1.tgz",
- "integrity": "sha512-+v5PcvrUcDeFXf8hv1gnNvNLdm4C0+2EiuWt9EatjjUmfriM1pTMM+r4j1lLHxeBQ9bVDmbywb11e3KjuavieA==",
+ "version": "4.8.0",
+ "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-4.8.0.tgz",
+ "integrity": "sha512-2zRpXDveJH8HsXkeeMtRW21do8wuSxVn1xXFdvhILyxlLWqGQrdJUA1/dk5DM7iAAYvwT/P3bDOLs90yL4S2AA==",
"requires": {
- "@octokit/types": "^6.1.0",
+ "@octokit/types": "^6.5.0",
"deprecation": "^2.3.1"
}
}
}
},
"@octokit/types": {
- "version": "6.3.2",
- "resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.3.2.tgz",
- "integrity": "sha512-H6cbnDumWOQJneyNKCBWgnktRqTWcEm6gq2cIS3frtVgpCqB8zguromnjIWJW375btjnxwmbYBTEAEouruZ2Yw==",
+ "version": "6.6.0",
+ "resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.6.0.tgz",
+ "integrity": "sha512-nmFoU3HCbw1AmnZU/eto2VvzV06+N7oAqXwMmAHGlNDF+KFykksh/VlAl85xc1P5T7Mw8fKYvXNaImNHCCH/rg==",
"requires": {
- "@octokit/openapi-types": "^2.3.1",
+ "@octokit/openapi-types": "^3.3.0",
"@types/node": ">= 8"
}
},
@@ -1111,9 +1139,9 @@
}
},
"@types/node": {
- "version": "14.14.21",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.21.tgz",
- "integrity": "sha512-cHYfKsnwllYhjOzuC5q1VpguABBeecUp24yFluHpn/BQaVxB1CuQ1FSRZCzrPxrkIfWISXV2LbeoBthLWg0+0A=="
+ "version": "14.14.22",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.22.tgz",
+ "integrity": "sha512-g+f/qj/cNcqKkc3tFqlXOYjrmZA+jNBiDzbP3kH+B+otKFqAdPgVTGP1IeKRdMml/aE69as5S4FqtxAbl+LaMw=="
},
"@types/normalize-package-data": {
"version": "2.4.0",
@@ -1877,11 +1905,6 @@
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
},
- "colors": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz",
- "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA=="
- },
"combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
@@ -2329,9 +2352,9 @@
}
},
"entities": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz",
- "integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w=="
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz",
+ "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A=="
},
"error-ex": {
"version": "1.3.2",
@@ -2704,10 +2727,9 @@
"dev": true
},
"faker": {
- "version": "5.1.0",
- "resolved": "https://registry.npmjs.org/faker/-/faker-5.1.0.tgz",
- "integrity": "sha512-RrWKFSSA/aNLP0g3o2WW1Zez7/MnMr7xkiZmoCfAGZmdkDQZ6l2KtuXHN5XjdvpRjDl8+3vf+Rrtl06Z352+Mw==",
- "dev": true
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/faker/-/faker-5.2.0.tgz",
+ "integrity": "sha512-UlrF1NNRIdzEPtBcy5l8JTlnXQZdz+4pQc3v2TAVocW39nnczCNQ0g0CBKgPGISJPzA2DqJVN1kdr+FCRFdN5g=="
},
"fast-deep-equal": {
"version": "3.1.3",
@@ -2785,9 +2807,9 @@
}
},
"follow-redirects": {
- "version": "1.13.1",
- "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.1.tgz",
- "integrity": "sha512-SSG5xmZh1mkPGyKzjZP8zLjltIfpW32Y5QpdNJyjcfGxK3qo3NDDkZOZSFiGn1A6SclQxY9GzEwAHQ3dmYRWpg=="
+ "version": "1.13.2",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.2.tgz",
+ "integrity": "sha512-6mPTgLxYm3r6Bkkg0vNM0HTjfGrOEtsfbhagQvbxDEsEkpNhw582upBaoRZylzen6krEmxXJgt9Ju6HiI4O7BA=="
},
"for-in": {
"version": "1.0.2",
@@ -2928,9 +2950,9 @@
"dev": true
},
"get-intrinsic": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.0.2.tgz",
- "integrity": "sha512-aeX0vrFm21ILl3+JpFFRNe9aUvp6VFZb2/CTbgLb8j75kOhvoNYjt9d8KA/tJG4gSo8nzEDedRl0h7vDmBYRVg==",
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.0.tgz",
+ "integrity": "sha512-M11rgtQp5GZMZzDL7jLTNxbDfurpzuau5uqRWDPvlHjfvg3TdScAZo96GLvhMjImrmR8uAt0FS2RLoMrfWGKlg==",
"requires": {
"function-bind": "^1.1.1",
"has": "^1.0.3",
@@ -4833,7 +4855,6 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.0.0.tgz",
"integrity": "sha512-pqon0s+4ScYUvX30wxQi3PogGFAlUyH0awepWvwkj4jD4v+ova3RiYw8bmA6x2rDrEaj8i/oWKoRxpVNW+Re8Q==",
- "dev": true,
"requires": {
"argparse": "^2.0.1"
},
@@ -4841,8 +4862,7 @@
"argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
- "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
- "dev": true
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
}
}
},
@@ -6460,6 +6480,31 @@
"integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==",
"dev": true
},
+ "simple-git": {
+ "version": "2.31.0",
+ "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-2.31.0.tgz",
+ "integrity": "sha512-/+rmE7dYZMbRAfEmn8EUIOwlM2G7UdzpkC60KF86YAfXGnmGtsPrKsym0hKvLBdFLLW019C+aZld1+6iIVy5xA==",
+ "requires": {
+ "@kwsites/file-exists": "^1.1.1",
+ "@kwsites/promise-deferred": "^1.1.1",
+ "debug": "^4.3.1"
+ },
+ "dependencies": {
+ "debug": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz",
+ "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==",
+ "requires": {
+ "ms": "2.1.2"
+ }
+ },
+ "ms": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
+ }
+ }
+ },
"sisteransi": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz",
diff --git a/package.json b/package.json
index c1b8ec9d..5de7ed42 100644
--- a/package.json
+++ b/package.json
@@ -6,7 +6,8 @@
"scripts": {
"start": "node source/app/web/index.mjs",
"test": "npx jest",
- "upgrade": "npm install @actions/core@latest @actions/github@latest @octokit/graphql@latest @octokit/rest@latest axios@latest colors@latest compression@latest ejs@latest express@latest express-rate-limit@latest image-to-base64@latest memory-cache@latest prismjs@latest puppeteer@latest svgo@latest vue@latest faker@latest jest@latest js-yaml@latest libxmljs@latest"
+ "index": "node .github/index.mjs",
+ "upgrade": "npm install @actions/core@latest @actions/github@latest @octokit/graphql@latest @octokit/rest@latest axios@latest compression@latest ejs@latest express@latest express-rate-limit@latest faker@latest image-to-base64@latest js-yaml@latest memory-cache@latest prismjs@latest puppeteer@latest svgo@latest vue@latest jest@latest libxmljs@latest"
},
"repository": {
"type": "git",
@@ -21,26 +22,26 @@
"dependencies": {
"@actions/core": "^1.2.6",
"@actions/github": "^4.0.0",
- "@octokit/graphql": "^4.5.8",
- "@octokit/rest": "^18.0.12",
+ "@octokit/graphql": "^4.5.9",
+ "@octokit/rest": "^18.0.15",
"axios": "^0.21.1",
- "colors": "^1.4.0",
"compression": "^1.7.4",
"ejs": "^3.1.5",
"express": "^4.17.1",
"express-rate-limit": "^5.2.3",
- "faker": "^5.1.0",
+ "faker": "^5.2.0",
"image-to-base64": "^2.1.1",
+ "js-yaml": "^4.0.0",
"memory-cache": "^0.2.0",
"prismjs": "^1.23.0",
"puppeteer": "^5.5.0",
+ "simple-git": "^2.31.0",
"svgo": "^1.3.2",
"vue": "^2.6.12",
"vue-prism-component": "^1.2.0"
},
"devDependencies": {
"jest": "^26.6.3",
- "js-yaml": "^4.0.0",
"libxmljs": "^0.19.7"
}
}
diff --git a/settings.example.json b/settings.example.json
index 0cd98447..bf074bf3 100644
--- a/settings.example.json
+++ b/settings.example.json
@@ -1,82 +1,82 @@
{
- "//":"This is an example of configuration file for web instance",
- "//":"It is not needed when using metrics as GitHub action",
+ "//": "Example of configuration for metrics web instance",
+ "//": "====================================================================",
- "token":"MY GITHUB API TOKEN", "//":"Your own GitHub API token (required)",
- "restricted":[], "//":"List of authorized users, leave empty for unrestricted",
- "cached":3600000, "//":"Cached time for generated images, 0 to disable",
- "maxusers":0, "//":"Maximum number of users, 0 for unlimited",
- "ratelimiter":null, "//":"Rate limiter (see express-rate-limit documentation for options)",
- "port":3000, "//":"Listening port",
- "optimize":true, "//":"Optimize SVG image",
- "debug":false, "//":"Debug mode",
- "mocked":false, "//":"Use mocked data",
- "repositories":100, "//":"Number of repositories to use to compute metrics",
- "community":{ "//":"Community settings",
- "templates":[], "//":"Community templates"
+ "token": "MY GITHUB API TOKEN", "//": "GitHub Personal Token (required)",
+ "restricted": [], "//": "Authorized users (empty to disable)",
+ "maxusers": 0, "//": "Maximum users, (0 to disable)",
+ "cached": 3600000, "//": "Cache time rendered metrics (0 to disable)",
+ "ratelimiter": null, "//": "Rate limiter (see express-rate-limit documentation)",
+ "port": 3000, "//": "Listening port",
+ "optimize": true, "//": "SVG optimization",
+ "debug": false, "//": "Debug logs",
+ "mocked": false, "//": "Use mocked data instead of live APIs",
+ "repositories": 100, "//": "Number of repositories to use",
+ "community": {
+ "templates": [], "//": "Additional community templates to setup"
},
- "templates":{ "//":"Template configuration",
- "default":"classic", "//":"Default template",
- "enabled":[], "//":"Enabled templates, leave empty to enable all templates"
+ "templates": {
+ "default": "classic", "//": "Default template",
+ "enabled": [], "//": "Enabled templates (empty to enable all)"
},
- "plugins":{ "//":"Additional plugins (optional)",
- "pagespeed":{ "//":"Pagespeed plugin",
- "enabled":false, "//":"Enable or disable PageSpeed metrics",
- "token":null, "//":"Pagespeed token (optional)"
+ "plugins": { "//": "Global plugin configuration",
+ "activity":{
+ "enabled": false, "//": "Display recent activity"
},
- "traffic":{ "//":"Traffic plugin (GitHub API token must be RW for this to work)",
- "enabled":false, "//":"Enable or disable repositories total page views is last two weeks"
+ "anilist":{
+ "enabled": false, "//": "Display data from your AniList account"
},
- "lines":{ "//":"Lines plugin",
- "enabled":false, "//":"Enable or disable repositories total lines added/removed"
+ "followup":{
+ "enabled": false, "//": "Display follow-up of repositories issues and pull requests"
},
- "habits":{ "//":"Habits plugin",
- "enabled":false, "//":"Enable or disable coding habits metrics",
- "from":200, "//":"Number of activity events to base habits on (up to 1000)"
+ "gists":{
+ "enabled": false, "//": "Display gists metrics"
},
- "languages":{ "//":"Languages plugin",
- "enabled":false, "//":"Enable or disable most used languages metrics"
+ "habits":{
+ "enabled": false, "//": "Display coding habits metrics"
},
- "followup":{ "//":"Follow-up plugin",
- "enabled":false, "//":"Enable or disable owned repositories issues and pull requests metrics"
+ "isocalendar":{
+ "enabled": false, "//": "Display an isometric view of your commits calendar"
},
- "music":{ "//":"Music plugin",
- "enabled":false, "//":"Enable or disable music recently played / random track from playlist",
- "token":null, "//":"Music provider token (may be required depending on provider)"
+ "languages":{
+ "enabled": false, "//": "Display most used languages metrics"
},
- "posts":{ "//":"Posts plugin",
- "enabled":false, "//":"Enable or disable recents posts"
+ "lines":{
+ "enabled": false, "//": "Display lines of code metrics"
},
- "isocalendar":{ "//":"Isometric calendar plugin",
- "enabled":false, "//":"Enable or disable isometric calendar"
+ "music":{
+ "token": null, "//":"Music provider personal token",
+ "enabled": false, "//": "Display your music tracks"
},
- "gists":{ "//":"Gists plugin",
- "enabled":false, "//":"Enable or disable gists metrics"
+ "pagespeed":{
+ "token": null, "//":"PageSpeed token",
+ "enabled": false, "//": "Display a website Google PageSpeed metrics"
},
- "topics":{ "//":"Topics plugin",
- "enabled":false, "//":"Enable or disable starred topics display"
+ "people":{
+ "enabled": false, "//": "Display GitHub users from various affiliations"
},
- "projects":{ "//":"Projects plugin",
- "enabled":false, "//":"Enable or disable personal projects display"
+ "posts":{
+ "enabled": false, "//": "Display recent posts"
},
- "tweets":{ "//":"Tweets plugin",
- "enabled":false, "//":"Enable or disable recent tweets display",
- "token":null, "//":"Twitter token (required when enabled)"
+ "projects":{
+ "enabled": false, "//": "Display active projects"
},
- "stars":{ "//":"Stars plugin",
- "enabled":false, "//":"Enable or disable recently starred repositories display"
+ "stargazers":{
+ "enabled": false, "//": "Display stargazers metrics"
},
- "stargazers":{ "//":"Stargazers plugin",
- "enabled":false, "//":"Enable or disable stargazers charts display"
+ "stars":{
+ "enabled": false, "//": "Display recently starred repositories"
},
- "activity":{ "//":"Activity plugin",
- "enabled":false, "//":"Enable or disable recent activity display"
+ "topics":{
+ "enabled": false, "//": "Display starred topics"
},
- "people":{ "//":"People plugin",
- "enabled":false, "//":"Enable or disable people display"
+ "traffic":{
+ "enabled": false, "//": "Display repositories traffic metrics"
},
- "anilist":{ "//":"Anilist plugin",
- "enabled":false, "//":"Enable or disable anilist display"
- }
+ "tweets":{
+ "token": null, "//":"Twitter API token",
+ "enabled": false, "//": "Display recent tweets"
+ },
+ "//": ""
}
}
\ No newline at end of file
diff --git a/source/app/action/action.yml b/source/app/action/action.yml
new file mode 100644
index 00000000..244e6dd0
--- /dev/null
+++ b/source/app/action/action.yml
@@ -0,0 +1,103 @@
+# ====================================================================================
+# Inputs and configuration
+
+inputs:
+<% for (const [plugin, {name, action}] of Object.entries(plugins)) { %>
+ # ====================================================================================
+ # <%- name %>
+ <% for (const [input, {comment, descriptor}] of Object.entries(action)) { %>
+ <%- comment.split("\n").map((line, i) => `${i ? " " : ""}${line}`).join("\n").trim() %>
+ <%- descriptor.split("\n").map((line, i) => `${i ? " " : ""}${line}`).join("\n") -%>
+<% }} %>
+
+# ====================================================================================
+# Action metadata
+name: GitHub metrics as SVG image
+author: lowlighter
+description: An SVG generator with 20+ metrics about your GitHub account! Additional plugins are available to display even more!
+branding:
+ icon: user-check
+ color: gray-dark
+
+# The action will parse its name to check if it's the official action or if it's a forked one
+# On the official action, it'll use the docker image published on GitHub registry when using a released version, allowing faster runs
+# On a forked action, it'll rebuild the docker image from Dockerfile to take into account changes you made
+runs:
+ using: composite
+ steps:
+ - run: |
+ # Create environment file from inputs and GitHub variables
+ cd $METRICS_ACTION_PATH
+ touch .env
+ for INPUT in $(echo $INPUTS | jq -r 'to_entries|map("INPUT_\(.key|ascii_upcase)=\(.value|@uri)")|.[]'); do
+ echo $INPUT >> .env
+ done
+ env | grep -E '^(GITHUB|ACTIONS|CI)' >> .env
+ echo "Environment variable: loaded"
+
+ # Source repository (picked from action name)
+ METRICS_SOURCE=$(echo $METRICS_ACTION | sed -E 's/metrics.*?$//g')
+ echo "Source: $METRICS_SOURCE"
+
+ # Version (picked from package.json)
+ METRICS_VERSION=$(grep -Po '(?<="version": ").*(?=")' package.json)
+ echo "Version: $METRICS_VERSION"
+
+ # Image tag (extracted from version or from env)
+ METRICS_TAG=v$(echo $METRICS_VERSION | sed -r 's/^([0-9]+[.][0-9]+).*/\1/')
+ if [[ $METRICS_USE_PREBUILT_IMAGE ]]; then
+ METRICS_TAG=$METRICS_USE_PREBUILT_IMAGE
+ echo "Pre-built image: yes"
+ fi
+ echo "Image tag: $METRICS_TAG"
+
+ # Image name
+ # Pre-built image
+ if [[ $METRICS_USE_PREBUILT_IMAGE ]]; then
+ echo "Using pre-built version $METRICS_TAG, will pull docker image from GitHub registry"
+ METRICS_IMAGE=ghcr.io/lowlighter/metrics:$METRICS_TAG
+ docker image pull $METRICS_IMAGE > /dev/null
+ # Official action
+ elif [[ $METRICS_SOURCE == "lowlighter" ]]; then
+ # Is unreleased version
+ set +e
+ METRICS_IS_RELEASED=$(expr $(expr match $METRICS_VERSION .*-beta) == 0)
+ set -e
+ echo "Is released version: $METRICS_IS_RELEASED"
+ # Rebuild image for unreleased version
+ if [[ "$METRICS_IS_RELEASED" -gt "0" ]]; then
+ echo "Using released version $METRICS_TAG, will pull docker image from GitHub registry"
+ METRICS_IMAGE=ghcr.io/lowlighter/metrics:$METRICS_TAG
+ # Use registry for released version
+ else
+ echo "Using an unreleased version ($METRICS_VERSION)"
+ METRICS_IMAGE=metrics:$METRICS_VERSION
+ fi
+ # Forked action
+ else
+ echo "Using a forked version"
+ METRICS_IMAGE=metrics:forked-$METRICS_VERSION
+ fi
+ echo "Image name: $METRICS_IMAGE"
+
+ # Build image if necessary
+ set +e
+ docker image inspect $METRICS_IMAGE > /dev/null
+ METRICS_IMAGE_NEEDS_BUILD="$?"
+ set -e
+ if [[ "$METRICS_IMAGE_NEEDS_BUILD" -gt "0" ]]; then
+ echo "Image $METRICS_IMAGE is not present locally, rebuilding it from Dockerfile"
+ docker build -t $METRICS_IMAGE . > /dev/null
+ else
+ echo "Image $METRICS_IMAGE is present locally"
+ fi
+
+ # Run docker image with current environment
+ docker run --volume $GITHUB_EVENT_PATH:$GITHUB_EVENT_PATH --env-file .env $METRICS_IMAGE
+ rm .env
+ shell: bash
+ env:
+ METRICS_ACTION: ${{ github.action }}
+ METRICS_ACTION_PATH: ${{ github.action_path }}
+ METRICS_USE_PREBUILT_IMAGE: ${{ inputs.use_prebuilt_image }}
+ INPUTS: ${{ toJson(inputs) }}
diff --git a/source/app/action/index.mjs b/source/app/action/index.mjs
index fd66b3a7..f480acf6 100644
--- a/source/app/action/index.mjs
+++ b/source/app/action/index.mjs
@@ -2,336 +2,225 @@
import core from "@actions/core"
import github from "@actions/github"
import octokit from "@octokit/graphql"
- import setup from "../setup.mjs"
- import mocks from "../mocks.mjs"
- import metrics from "../metrics.mjs"
+ import setup from "../metrics/setup.mjs"
+ import mocks from "../mocks/index.mjs"
+ import metrics from "../metrics/index.mjs"
-;((async function () {
- //Input parser
- const input = {
- get:(name) => {
- const value = `${core.getInput(name)}`.trim()
- try { return decodeURIComponent(value) }
- catch { return value}
- },
- bool:(name, {default:defaulted = undefined} = {}) => /^(?:[Tt]rue|[Oo]n|[Yy]es)$/.test(input.get(name)) ? true : /^(?:[Ff]alse|[Oo]ff|[Nn]o)$/.test(input.get(name)) ? false : defaulted,
- number:(name, {default:defaulted = undefined} = {}) => Number.isFinite(Number(input.get(name))) ? Number(input.get(name)) : defaulted,
- string:(name, {default:defaulted = undefined} = {}) => input.get(name) || defaulted,
- array:(name, {separator = ","} = {}) => input.get(name).split(separator).map(value => value.trim()).filter(value => value),
- object:(name) => JSON.parse(input.get(name) || "{}"),
- }
- //Info logger
- const info = (left, right, {token = false} = {}) => console.log(`${`${left}`.padEnd(48)} โ ${
- Array.isArray(right) ? right.join(", ") || "(none)" :
- right === undefined ? "(default)" :
- token ? /^MOCKED/.test(right) ? "(MOCKED TOKEN)" : (right ? "(provided)" : "(missing)") :
- typeof right === "object" ? JSON.stringify(right) :
- right
- }`)
- //Debug message buffer
- const debugged = []
- //Runner
- try {
- //Initialization
- console.log("โ".repeat(64))
- console.log(`Metrics`)
- console.log("โ".repeat(64))
- process.on("unhandledRejection", error => { throw error })
+//Debug message buffer
+ let DEBUG = true
+ const debugged = []
- //Skip process if needed
- if ((github.context.eventName === "push")&&(github.context.payload?.head_commit)) {
- if (/\[Skip GitHub Action\]/.test(github.context.payload.head_commit.message)) {
- console.log(`Skipped because [Skip GitHub Action] is in commit message`)
- process.exit(0)
- }
+//Info logger
+ const info = (left, right, {token = false} = {}) => console.log(`${`${left}`.padEnd(56 + 9*(/0m$/.test(left)))} โ ${
+ Array.isArray(right) ? right.join(", ") || "(none)" :
+ right === undefined ? "(default)" :
+ token ? /^MOCKED/.test(right) ? "(MOCKED TOKEN)" : (right ? "(provided)" : "(missing)") :
+ typeof right === "object" ? JSON.stringify(right) :
+ right
+ }`)
+ info.section = (left = "", right = " ") => info(`\x1b[36m${left}\x1b[0m`, right)
+ info.group = ({metadata, name, inputs}) => {
+ info.section(metadata.plugins[name]?.name?.match(/(?[\w\s]+)/i)?.groups?.section?.trim(), " ")
+ for (const [input, value] of Object.entries(inputs))
+ info(metadata.plugins[name]?.inputs[input]?.description ?? input, value, {token:metadata.plugins[name]?.inputs[input]?.type === "token"})
+ }
+ info.break = () => console.log("โ".repeat(88))
+
+//Runner
+ try {
+ //Initialization
+ info.break()
+ info.section(`Metrics`)
+ process.on("unhandledRejection", error => { throw error })
+
+ //Skip process if needed
+ if ((github.context.eventName === "push")&&(github.context.payload?.head_commit)) {
+ if (/\[Skip GitHub Action\]/.test(github.context.payload.head_commit.message)) {
+ console.log(`Skipped because [Skip GitHub Action] is in commit message`)
+ process.exit(0)
}
+ }
- //Pre-Setup
- const community = {
- templates:input.array("setup_community_templates")
- }
- info("Setup - community templates", community.templates)
+ //Load configuration
+ const {conf, Plugins, Templates} = await setup({log:false, nosettings:true, community:{templates:core.getInput("setup_community_templates")}})
+ const {metadata} = conf
+ info("Setup", "complete")
+ info("Version", conf.package.version)
- //Load configuration
- const {conf, Plugins, Templates} = await setup({log:false, nosettings:true, community})
- info("Setup", "complete")
- info("Version", conf.package.version)
+ //Core inputs
+ const {
+ user:_user, token,
+ template, query, "setup.community.templates":_templates,
+ filename, optimize, verify,
+ debug, "debug.flags":dflags, "use.mocked.data":mocked, dryrun,
+ "plugins.errors.fatal":die,
+ "committer.token":_token, "committer.branch":_branch,
+ "use.prebuilt.image":_image,
+ ...config
+ } = metadata.plugins.core.inputs.action({core})
+ const q = {...query, template}
- //Debug mode
- const debug = input.bool("debug", {default:false})
- info("Debug mode", debug)
- if (!debug)
- console.debug = message => debugged.push(message)
- const dflags = input.array("debug_flags", {separator:" "})
- info("Debug flags", dflags)
+ //Docker image
+ if (_image)
+ info("Using prebuilt image", image)
- //Load svg template, style, fonts and query
- const template = input.string("template", {default:"classic"})
- info("Template used", template)
+ //Debug mode and flags
+ info("Debug mode", debug)
+ if (!debug) {
+ console.debug = message => debugged.push(message)
+ DEBUG = false
+ }
+ info("Debug flags", dflags)
- //Token for data gathering
- const token = input.string("token")
- info("GitHub token", token, {token:true})
- if (!token)
- throw new Error("You must provide a valid GitHub token to gather your metrics")
- const api = {}
- api.graphql = octokit.graphql.defaults({headers:{authorization: `token ${token}`}})
- info("Github GraphQL API", "ok")
- api.rest = github.getOctokit(token)
- info("Github REST API", "ok")
- //Apply mocking if needed
- if (input.bool("use_mocked_data", {default:false})) {
- Object.assign(api, await mocks(api))
- info("Use mocked API", true)
- }
- //Extract octokits
- const {graphql, rest} = api
+ //Token for data gathering
+ info("GitHub token", token, {token:true})
+ if (!token)
+ throw new Error("You must provide a valid GitHub token to gather your metrics")
+ const api = {}
+ api.graphql = octokit.graphql.defaults({headers:{authorization: `token ${token}`}})
+ info("Github GraphQL API", "ok")
+ api.rest = github.getOctokit(token)
+ info("Github REST API", "ok")
+ //Apply mocking if needed
+ if (mocked) {
+ Object.assign(api, await mocks(api))
+ info("Use mocked API", true)
+ }
+ //Extract octokits
+ const {graphql, rest} = api
- //SVG output
- const filename = input.string("filename", {default:"github-metrics.svg"})
- info("SVG output", filename)
+ //GitHub user
+ let authenticated
+ try {
+ authenticated = (await rest.users.getAuthenticated()).data.login
+ }
+ catch {
+ authenticated = github.context.repo.owner
+ }
+ const user = _user || authenticated
+ info("GitHub account", user)
- //SVG optimization
- const optimize = input.bool("optimize", {default:true})
- conf.optimize = optimize
- info("SVG optimization", optimize)
+ //Current repository
+ info("Current repository", `${github.context.repo.owner}/${github.context.repo.repo}`)
- //Verify svg
- const verify = input.bool("verify")
- info("SVG verification after generation", verify)
-
- //GitHub user
- let authenticated
- try {
- authenticated = (await rest.users.getAuthenticated()).data.login
- }
- catch {
- authenticated = github.context.repo.owner
- }
- const user = input.string("user", {default:authenticated})
- info("Target GitHub user", user)
-
- //Base elements
- const base = {}
- const parts = input.array("base")
- for (const part of conf.settings.plugins.base.parts)
- base[`base.${part}`] = parts.includes(part)
- info("Base parts", parts)
-
- //Config
- const config = {
- "config.timezone":input.string("config_timezone"),
- "config.output":input.string("config_output"),
- "config.animations":input.bool("config_animations"),
- "config.padding":input.string("config_padding"),
- "config.order":input.array("config_order"),
- }
- info("Timezone", config["config.timezone"] ?? "(system default)")
- info("Convert SVG", config["config.output"] ?? "(no)")
- info("Enable SVG animations", config["config.animations"])
- info("SVG bottom padding", config["config.padding"])
- info("Content order", config["config.order"])
-
- //Additional plugins
- const plugins = {
- lines:{enabled:input.bool("plugin_lines")},
- traffic:{enabled:input.bool("plugin_traffic")},
- pagespeed:{enabled:input.bool("plugin_pagespeed")},
- habits:{enabled:input.bool("plugin_habits")},
- languages:{enabled:input.bool("plugin_languages")},
- followup:{enabled:input.bool("plugin_followup")},
- music:{enabled:input.bool("plugin_music")},
- posts:{enabled:input.bool("plugin_posts")},
- isocalendar:{enabled:input.bool("plugin_isocalendar")},
- gists:{enabled:input.bool("plugin_gists")},
- topics:{enabled:input.bool("plugin_topics")},
- projects:{enabled:input.bool("plugin_projects")},
- tweets:{enabled:input.bool("plugin_tweets")},
- stars:{enabled:input.bool("plugin_stars")},
- stargazers:{enabled:input.bool("plugin_stargazers")},
- activity:{enabled:input.bool("plugin_activity")},
- people:{enabled:input.bool("plugin_people")},
- anilist:{enabled:input.bool("plugin_anilist")},
- }
- let q = Object.fromEntries(Object.entries(plugins).filter(([key, plugin]) => plugin.enabled).map(([key]) => [key, true]))
- info("Plugins enabled", Object.entries(plugins).filter(([key, plugin]) => plugin.enabled).map(([key]) => key))
- //Additional plugins options
- //Pagespeed
- if (plugins.pagespeed.enabled) {
- plugins.pagespeed.token = input.string("plugin_pagespeed_token")
- info("Pagespeed token", plugins.pagespeed.token, {token:true})
- for (const option of ["url"])
- info(`Pagespeed ${option}`, q[`pagespeed.${option}`] = input.string(`plugin_pagespeed_${option}`))
- for (const option of ["detailed", "screenshot"])
- info(`Pagespeed ${option}`, q[`pagespeed.${option}`] = input.bool(`plugin_pagespeed_${option}`))
+ //Committer
+ const committer = {}
+ if (!dryrun) {
+ //Compute committer informations
+ committer.commit = true
+ committer.token = _token || token
+ committer.branch = _branch || github.context.ref.replace(/^refs[/]heads[/]/, "")
+ info("Committer token", committer.token, {token:true})
+ if (!committer.token)
+ throw new Error("You must provide a valid GitHub token to commit your metrics")
+ info("Committer branch", committer.branch)
+ //Instantiate API for committer
+ committer.rest = github.getOctokit(committer.token)
+ info("Committer REST API", "ok")
+ try {
+ info("Committer account", (await committer.rest.users.getAuthenticated()).data.login)
}
- //Languages
- if (plugins.languages.enabled) {
- for (const option of ["ignored", "skipped", "colors"])
- info(`Languages ${option}`, q[`languages.${option}`] = input.array(`plugin_languages_${option}`))
+ catch {
+ info("Committer account", "(github-actions)")
}
- //Habits
- if (plugins.habits.enabled) {
- for (const option of ["facts", "charts"])
- info(`Habits ${option}`, q[`habits.${option}`] = input.bool(`plugin_habits_${option}`))
- for (const option of ["from", "days"])
- info(`Habits ${option}`, q[`habits.${option}`] = input.number(`plugin_habits_${option}`))
- }
- //Music
- if (plugins.music.enabled) {
- plugins.music.token = input.string("plugin_music_token")
- info("Music token", plugins.music.token, {token:true})
- for (const option of ["provider", "mode", "playlist", "user"])
- info(`Music ${option}`, q[`music.${option}`] = input.string(`plugin_music_${option}`))
- for (const option of ["limit"])
- info(`Music ${option}`, q[`music.${option}`] = input.number(`plugin_music_${option}`))
- }
- //Posts
- if (plugins.posts.enabled) {
- for (const option of ["source", "user"])
- info(`Posts ${option}`, q[`posts.${option}`] = input.string(`plugin_posts_${option}`))
- for (const option of ["limit"])
- info(`Posts ${option}`, q[`posts.${option}`] = input.number(`plugin_posts_${option}`))
- }
- //Isocalendar
- if (plugins.isocalendar.enabled) {
- for (const option of ["duration"])
- info(`Isocalendar ${option}`, q[`isocalendar.${option}`] = input.string(`plugin_isocalendar_${option}`))
- }
- //Topics
- if (plugins.topics.enabled) {
- for (const option of ["mode", "sort"])
- info(`Topics ${option}`, q[`topics.${option}`] = input.string(`plugin_topics_${option}`))
- for (const option of ["limit"])
- info(`Topics ${option}`, q[`topics.${option}`] = input.number(`plugin_topics_${option}`))
- }
- //Projects
- if (plugins.projects.enabled) {
- for (const option of ["repositories"])
- info(`Projects ${option}`, q[`projects.${option}`] = input.string(`plugin_projects_${option}`))
- for (const option of ["limit"])
- info(`Projects ${option}`, q[`projects.${option}`] = input.number(`plugin_projects_${option}`))
- for (const option of ["descriptions"])
- info(`Projects ${option}`, q[`projects.${option}`] = input.bool(`plugin_projects_${option}`))
- }
- //Tweets
- if (plugins.tweets.enabled) {
- plugins.tweets.token = input.string("plugin_tweets_token")
- info("Tweets token", plugins.tweets.token, {token:true})
- for (const option of ["user"])
- info(`Tweets ${option}`, q[`tweets.${option}`] = input.string(`plugin_tweets_${option}`))
- for (const option of ["limit"])
- info(`Tweets ${option}`, q[`tweets.${option}`] = input.number(`plugin_tweets_${option}`))
- }
- //Stars
- if (plugins.stars.enabled) {
- for (const option of ["limit"])
- info(`Stars ${option}`, q[`stars.${option}`] = input.number(`plugin_stars_${option}`))
- }
- //Activity
- if (plugins.activity.enabled) {
- for (const option of ["limit", "days"])
- info(`Activity ${option}`, q[`activity.${option}`] = input.number(`plugin_activity_${option}`))
- for (const option of ["filter"])
- info(`Activity ${option}`, q[`activity.${option}`] = input.array(`plugin_activity_${option}`))
- }
- //People
- if (plugins.people.enabled) {
- for (const option of ["limit", "size"])
- info(`People ${option}`, q[`people.${option}`] = input.number(`plugin_people_${option}`))
- for (const option of ["types", "thanks"])
- info(`People ${option}`, q[`people.${option}`] = input.array(`plugin_people_${option}`))
- for (const option of ["identicons"])
- info(`People ${option}`, q[`people.${option}`] = input.bool(`plugin_people_${option}`))
- }
- //Anilist
- if (plugins.anilist.enabled) {
- for (const option of ["limit"])
- info(`Anilist ${option}`, q[`anilist.${option}`] = input.number(`plugin_anilist_${option}`))
- for (const option of ["medias", "sections"])
- info(`Anilist ${option}`, q[`anilist.${option}`] = input.array(`plugin_anilist_${option}`))
- for (const option of ["shuffle"])
- info(`Anilist ${option}`, q[`anilist.${option}`] = input.bool(`plugin_anilist_${option}`))
- for (const option of ["user"])
- info(`Anilist ${option}`, q[`anilist.${option}`] = input.string(`plugin_anilist_${option}`))
- }
-
- //Repositories to use
- const repositories = input.number("repositories")
- const forks = input.bool("repositories_forks")
- info("Repositories to process", repositories)
- info("Include forked repositories", forks)
-
- //Die on plugins errors
- const die = input.bool("plugins_errors_fatal")
- info("Plugin errors", die ? "(exit with error)" : "(displayed in generated SVG)")
-
- //Build query
- const query = input.object("query")
- info("Query additional params", query)
- q = {...query, ...q, base:false, ...base, ...config, repositories, "repositories.forks":forks, template}
-
- //Render metrics
- const {rendered} = await metrics({login:user, q, dflags}, {graphql, rest, plugins, conf, die, verify}, {Plugins, Templates})
- info("Rendering", "complete")
-
- //Commit to repository
- const dryrun = input.bool("dryrun")
- if (dryrun)
- info("Dry-run", "complete")
- else {
- //Repository and branch
- const branch = input.string("committer_branch", {default:github.context.ref.replace(/^refs[/]heads[/]/, "")})
- info("Current repository", `${github.context.repo.owner}/${github.context.repo.repo}`)
- info("Current branch", branch)
- //Committer token
- const token = input.string("committer_token", {default:input.string("token")})
- info("Committer token", token, {token:true})
- if (!token)
- throw new Error("You must provide a valid GitHub token to commit your metrics")
- const rest = github.getOctokit(token)
- info("Committer REST API", "ok")
- try {
- info("Committer", (await rest.users.getAuthenticated()).data.login)
- }
- catch {
- info("Committer", "(github-actions)")
- }
- //Retrieve previous render SHA to be able to update file content through API
- let sha = null
- try {
- const {repository:{object:{oid}}} = await graphql(`
- query Sha {
- repository(owner: "${github.context.repo.owner}", name: "${github.context.repo.repo}") {
- object(expression: "${branch}:${filename}") { ... on Blob { oid } }
- }
+ //Retrieve previous render SHA to be able to update file content through API
+ committer.sha = null
+ try {
+ const {repository:{object:{oid}}} = await graphql(`
+ query Sha {
+ repository(owner: "${github.context.repo.owner}", name: "${github.context.repo.repo}") {
+ object(expression: "${committer.branch}:${filename}") { ... on Blob { oid } }
}
- `
- )
- sha = oid
- } catch (error) { console.debug(error) }
- info("Previous render sha", sha ?? "(none)")
- //Update file content through API
- await rest.repos.createOrUpdateFileContents({
- ...github.context.repo, path:filename, message:`Update ${filename} - [Skip GitHub Action]`,
- content:Buffer.from(rendered).toString("base64"),
- branch,
- ...(sha ? {sha} : {})
- })
- info("Commit to current repository", "ok")
- }
+ }
+ `
+ )
+ committer.sha = oid
+ } catch (error) { console.debug(error) }
+ info("Previous render sha", committer.sha ?? "(none)")
+ }
+ else
+ info("Dry-run", true)
- //Success
- console.log(`Success, thanks for using metrics !`)
- process.exit(0)
- }
- //Errors
- catch (error) {
- console.error(error)
- if (!input.bool("debug"))
- for (const log of ["โ".repeat(64), "An error occured, logging debug message :", ...debugged])
+ //SVG file
+ conf.optimize = optimize
+ info("SVG output", filename)
+ info("SVG optimization", optimize)
+ info("SVG verification after generation", verify)
+
+ //Template
+ info.break()
+ info.section("Templates")
+ info("Community templates", _templates)
+ info("Template used", template)
+ info("Query additional params", query)
+
+ //Core config
+ info.break()
+ info.group({metadata, name:"core", inputs:config})
+ info("Plugin errors", die ? "(exit with error)" : "(displayed in generated SVG)")
+ Object.assign(q, config)
+
+ //Base content
+ info.break()
+ const {base:parts, ...base} = metadata.plugins.base.inputs.action({core})
+ info.group({metadata, name:"base", inputs:base})
+ info("Base sections", parts)
+ base.base = false
+ for (const part of conf.settings.plugins.base.parts)
+ base[`base.${part}`] = parts.includes(part)
+ Object.assign(q, base)
+
+ //Additional plugins
+ const plugins = {}
+ for (const name of Object.keys(Plugins).filter(key => !["base", "core"].includes(key))) {
+ //Parse inputs
+ const {[name]:enabled, ...inputs} = metadata.plugins[name].inputs.action({core})
+ plugins[name] = {enabled}
+ //Register user inputs
+ if (enabled) {
+ info.break()
+ info.group({metadata, name, inputs:enabled ? inputs : {}})
+ q[name] = true
+ for (const [key, value] of Object.entries(inputs)) {
+ //Store token in plugin configuration
+ if (metadata.plugins[name].inputs[key].type === "token")
+ plugins[name][key] = value
+ //Store value in query
+ else
+ q[`${name}.${key}`] = value
+ }
+ }
+ }
+
+ //Render metrics
+ info.break()
+ info.section("Rendering")
+ const {rendered} = await metrics({login:user, q, dflags}, {graphql, rest, plugins, conf, die, verify}, {Plugins, Templates})
+ info("Status", "complete")
+
+ //Commit metrics
+ if (committer.commit) {
+ await committer.rest.repos.createOrUpdateFileContents({
+ ...github.context.repo, path:filename, message:`Update ${filename} - [Skip GitHub Action]`,
+ content:Buffer.from(rendered).toString("base64"),
+ branch:committer.branch,
+ ...(committer.sha ? {sha:committer.sha} : {})
+ })
+ info("Commit to repository", "success")
+ }
+
+ //Success
+ info.break()
+ console.log(`Success, thanks for using metrics!`)
+ process.exit(0)
+ }
+//Errors
+ catch (error) {
+ console.error(error)
+ //Print debug buffer if debug was not enabled (if it is, it's already logged on the fly)
+ if (!DEBUG)
+ for (const log of [info.break(), "An error occured, logging debug message :", ...debugged])
console.log(log)
- core.setFailed(error.message)
- process.exit(1)
- }
-})()).catch(error => process.exit(1))
\ No newline at end of file
+ core.setFailed(error.message)
+ process.exit(1)
+ }
diff --git a/source/app/metrics.mjs b/source/app/metrics.mjs
deleted file mode 100644
index 74fa1d6f..00000000
--- a/source/app/metrics.mjs
+++ /dev/null
@@ -1,289 +0,0 @@
-//Imports
- import fs from "fs/promises"
- import os from "os"
- import paths from "path"
- import util from "util"
- import axios from "axios"
- import url from "url"
- import puppeteer from "puppeteer"
- import processes from "child_process"
- import ejs from "ejs"
- import imgb64 from "image-to-base64"
- import SVGO from "svgo"
-
-//Setup
- export default async function metrics({login, q, dflags = []}, {graphql, rest, plugins, conf, die = false, verify = false, convert = null}, {Plugins, Templates}) {
- //Compute rendering
- try {
-
- //Init
- console.debug(`metrics/compute/${login} > start`)
- console.debug(util.inspect(q, {depth:Infinity, maxStringLength:256}))
- const template = q.template || conf.settings.templates.default
- const repositories = Math.max(0, Number(q.repositories)) || conf.settings.repositories || 100
- const pending = []
- if ((!(template in Templates))||(!(template in conf.templates))||((conf.settings.templates.enabled.length)&&(!conf.settings.templates.enabled.includes(template))))
- throw new Error("unsupported template")
- const {image, style, fonts, views, partials} = conf.templates[template]
- const queries = conf.queries
- const data = {animated:true, base:{}, config:{}, errors:[], plugins:{}, computed:{}}
- const s = (value, end = "") => value !== 1 ? {y:"ies", "":"s"}[end] : end
-
- //Base parts
- {
- 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
- }
- //Partial parts
- {
- data.partials = new Set([
- ...decodeURIComponent(q["config.order"] ?? "").split(",").map(x => x.trim().toLocaleLowerCase()).filter(partial => partials.includes(partial)),
- ...partials,
- ])
- console.debug(`metrics/compute/${login} > content order : ${[...data.partials]}`)
- }
-
- //Query data from GitHub API
- await common({login, q, data, queries, repositories, graphql})
- //Compute metrics
- console.debug(`metrics/compute/${login} > compute`)
- const computer = Templates[template].default || Templates[template]
- await computer({login, q, dflags}, {conf, data, rest, graphql, plugins, queries, account:data.account}, {s, pending, imports:{plugins:Plugins, url, imgb64, axios, puppeteer, run, fs, os, paths, util, format, bytes, shuffle, htmlescape, urlexpand, __module}})
- const promised = await Promise.all(pending)
-
- //Check plugins errors
- {
- const errors = [...promised.filter(({result = null}) => result?.error), ...data.errors]
- if (errors.length) {
- console.warn(`metrics/compute/${login} > ${errors.length} errors !`)
- if (die)
- throw new Error(`An error occured during rendering, dying`)
- else
- console.warn(util.inspect(errors, {depth:Infinity, maxStringLength:256}))
- }
- }
-
- //Template rendering
- console.debug(`metrics/compute/${login} > render`)
- let rendered = await ejs.render(image, {...data, s, f:format, style, fonts}, {views, async:true})
- //Apply resizing
- const {resized, mime} = await svgresize(rendered, {paddings:q["config.padding"], convert})
- rendered = resized
-
- //Additional SVG transformations
- if (/svg/.test(mime)) {
- //Optimize rendering
- if ((conf.settings?.optimize)&&(!q.raw)) {
- console.debug(`metrics/compute/${login} > optimize`)
- const svgo = new SVGO({full:true, plugins:[{cleanupAttrs:true}, {inlineStyles:false}]})
- const {data:optimized} = await svgo.optimize(rendered)
- rendered = optimized
- }
- //Verify svg
- if (verify) {
- console.debug(`metrics/compute/${login} > verify SVG`)
- const libxmljs = (await import("libxmljs")).default
- const parsed = libxmljs.parseXml(rendered)
- if (parsed.errors.length)
- throw new Error(`Malformed SVG : \n${parsed.errors.join("\n")}`)
- }
- }
-
- //Result
- console.debug(`metrics/compute/${login} > success`)
- return {rendered, mime}
- }
- //Internal error
- catch (error) {
- //User not found
- if (((Array.isArray(error.errors))&&(error.errors[0].type === "NOT_FOUND")))
- throw new Error("user not found")
- //Generic error
- throw error
- }
- }
-
-/** Common query */
- async function common({login, q, data, queries, repositories, graphql}) {
- //Iterate through account types
- for (const account of ["user", "organization"]) {
- try {
- //Query data from GitHub API
- console.debug(`metrics/compute/${login}/common > account ${account}`)
- const forks = q["repositories.forks"] || false
- const queried = await graphql(queries[{user:"common", organization:"common.organization"}[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]})
- common.post?.[account]({login, data})
- //Query repositories from GitHub API
- {
- //Iterate through repositories
- let cursor = null
- let pushed = 0
- do {
- console.debug(`metrics/compute/${login}/common > retrieving repositories after ${cursor}`)
- const {[account]:{repositories:{edges, nodes}}} = await graphql(queries.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}/common > keeping only ${repositories} repositories`)
- data.user.repositories.nodes.splice(repositories)
- console.debug(`metrics/compute/${login}/common > loaded ${data.user.repositories.nodes.length} repositories`)
- }
- //Success
- console.debug(`metrics/compute/${login}/common > graphql query > account ${account} > success`)
- return
- } catch (error) {
- console.debug(`metrics/compute/${login}/common > account ${account} > failed : ${error}`)
- console.debug(`metrics/compute/${login}/common > checking next account`)
- }
- }
- //Not found
- console.debug(`metrics/compute/${login}/common > no more account type`)
- throw new Error("user not found")
- }
-
-/** Common query post-processing */
- common.post = {
- //User
- user({login, data}) {
- console.debug(`metrics/compute/${login}/common > applying common post`)
- data.account = "user"
- Object.assign(data.user, {
- isVerified:false,
- })
- },
- //Organization
- organization({login, data}) {
- console.debug(`metrics/compute/${login}/common > applying common post`)
- 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},
- })
- }
- }
-
-/** Returns module __dirname */
- function __module(module) {
- return paths.join(paths.dirname(url.fileURLToPath(module)))
- }
-
-/** Formatter */
- function format(n, {sign = false} = {}) {
- for (const {u, v} of [{u:"b", v:10**9}, {u:"m", v:10**6}, {u:"k", v:10**3}])
- if (n/v >= 1)
- return `${(sign)&&(n > 0) ? "+" : ""}${(n/v).toFixed(2).substr(0, 4).replace(/[.]0*$/, "")}${u}`
- return `${(sign)&&(n > 0) ? "+" : ""}${n}`
- }
-
-/** Bytes formatter */
- function bytes(n) {
- for (const {u, v} of [{u:"E", v:10**18}, {u:"P", v:10**15}, {u:"T", v:10**12}, {u:"G", v:10**9}, {u:"M", v:10**6}, {u:"k", v:10**3}])
- if (n/v >= 1)
- return `${(n/v).toFixed(2).substr(0, 4).replace(/[.]0*$/, "")} ${u}B`
- return `${n} byte${n > 1 ? "s" : ""}`
- }
-
-/** Array shuffler */
- function shuffle(array) {
- for (let i = array.length-1; i > 0; i--) {
- const j = Math.floor(Math.random()*(i+1))
- ;[array[i], array[j]] = [array[j], array[i]]
- }
- return array
- }
-
-/** Escape html */
- function htmlescape(string, u = {"&":true, "<":true, ">":true, '"':true, "'":true}) {
- return string
- .replace(/&(?!(?:amp|lt|gt|quot|apos);)/g, u["&"] ? "&" : "&")
- .replace(//g, u[">"] ? ">" : ">")
- .replace(/"/g, u['"'] ? """ : '"')
- .replace(/'/g, u["'"] ? "'" : "'")
- }
-
-/** Expand url */
- async function urlexpand(url) {
- try {
- return (await axios.get(url)).request.res.responseUrl
- } catch {
- return url
- }
- }
-
-/** Run command */
- async function run(command, options) {
- return await new Promise((solve, reject) => {
- console.debug(`metrics/command > ${command}`)
- const child = processes.exec(command, options)
- let [stdout, stderr] = ["", ""]
- child.stdout.on("data", data => stdout += data)
- child.stderr.on("data", data => stderr += data)
- child.on("close", code => {
- console.debug(`metrics/command > ${command} > exited with code ${code}`)
- return code === 0 ? solve(stdout) : reject(stderr)
- })
- })
- }
-
-/** Render svg */
- async function svgresize(svg, {paddings = "6%", convert} = {}) {
- //Instantiate browser if needed
- if (!svgresize.browser) {
- svgresize.browser = await 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/svgresize > started ${await svgresize.browser.version()}`)
- }
- //Format padding
- const [pw = 1, ph] = paddings.split(",").map(padding => `${padding}`.substring(0, padding.length-1)).map(value => 1+Number(value)/100)
- const padding = {width:pw, height:ph ?? pw}
- console.debug(`metrics/svgresize > padding width*${padding.width}, height*${padding.height}`)
- //Render through browser and resize height
- const page = await svgresize.browser.newPage()
- await page.setContent(svg, {waitUntil:"load"})
- let mime = "image/svg+xml"
- let {resized, width, height} = await page.evaluate(async padding => {
- //Disable animations
- const animated = !document.querySelector("svg").classList.contains("no-animations")
- if (animated)
- document.querySelector("svg").classList.add("no-animations")
- //Get bounds and resize
- let {y:height, width} = document.querySelector("svg #metrics-end").getBoundingClientRect()
- height = Math.ceil(height*padding.height)
- width = Math.ceil(width*padding.width)
- //Resize svg
- document.querySelector("svg").setAttribute("height", height)
- //Enable animations
- if (animated)
- document.querySelector("svg").classList.remove("no-animations")
- //Result
- return {resized:new XMLSerializer().serializeToString(document.querySelector("svg")), height, width}
- }, padding)
- //Convert if required
- if (convert) {
- console.debug(`metrics/svgresize > convert to ${convert}`)
- resized = await page.screenshot({type:convert, clip:{x:0, y:0, width, height}, omitBackground:true})
- mime = `image/${convert}`
- }
- //Result
- await page.close()
- return {resized, mime}
- }
diff --git a/source/app/metrics/index.mjs b/source/app/metrics/index.mjs
new file mode 100644
index 00000000..6bd095e1
--- /dev/null
+++ b/source/app/metrics/index.mjs
@@ -0,0 +1,92 @@
+//Imports
+ import util from "util"
+ import ejs from "ejs"
+ import SVGO from "svgo"
+ import * as utils from "./utils.mjs"
+
+//Setup
+ export default async function metrics({login, q, dflags = []}, {graphql, rest, plugins, conf, die = false, verify = false, convert = null}, {Plugins, Templates}) {
+ //Compute rendering
+ try {
+
+ //Debug
+ console.debug(`metrics/compute/${login} > start`)
+ console.debug(util.inspect(q, {depth:Infinity, maxStringLength:256}))
+
+ //Load template
+ const template = q.template || conf.settings.templates.default
+ if ((!(template in Templates))||(!(template in conf.templates))||((conf.settings.templates.enabled.length)&&(!conf.settings.templates.enabled.includes(template))))
+ throw new Error("unsupported template")
+ const {image, style, fonts, views, partials} = conf.templates[template]
+ const computer = Templates[template].default || Templates[template]
+
+ //Initialization
+ const pending = []
+ const queries = conf.queries
+ const data = {animated:true, base:{}, config:{}, errors:[], plugins:{}, computed:{}}
+ const imports = {plugins:Plugins, templates:Templates, metadata:conf.metadata, ...utils}
+
+ //Partial parts
+ {
+ data.partials = new Set([
+ ...decodeURIComponent(q["config.order"] ?? "").split(",").map(x => x.trim().toLocaleLowerCase()).filter(partial => partials.includes(partial)),
+ ...partials,
+ ])
+ console.debug(`metrics/compute/${login} > content order : ${[...data.partials]}`)
+ }
+
+ //Executing base plugin and compute metrics
+ console.debug(`metrics/compute/${login} > compute`)
+ await Plugins.base({login, q, data, rest, graphql, plugins, queries, pending, imports}, conf)
+ await computer({login, q, dflags}, {conf, data, rest, graphql, plugins, queries, account:data.account}, {pending, imports})
+ const promised = await Promise.all(pending)
+
+ //Check plugins errors
+ const errors = [...promised.filter(({result = null}) => result?.error), ...data.errors]
+ if (errors.length) {
+ console.warn(`metrics/compute/${login} > ${errors.length} errors !`)
+ if (die)
+ throw new Error(`An error occured during rendering, dying`)
+ else
+ console.warn(util.inspect(errors, {depth:Infinity, maxStringLength:256}))
+ }
+
+ //Rendering and resizing
+ console.debug(`metrics/compute/${login} > render`)
+ let rendered = await ejs.render(image, {...data, s:imports.s, f:imports.format, style, fonts}, {views, async:true})
+ const {resized, mime} = await imports.svgresize(rendered, {paddings:q["config.padding"], convert})
+ rendered = resized
+
+ //Additional SVG transformations
+ if (/svg/.test(mime)) {
+ //Optimize rendering
+ if ((conf.settings?.optimize)&&(!q.raw)) {
+ console.debug(`metrics/compute/${login} > optimize`)
+ const svgo = new SVGO({full:true, plugins:[{cleanupAttrs:true}, {inlineStyles:false}]})
+ const {data:optimized} = await svgo.optimize(rendered)
+ rendered = optimized
+ }
+ //Verify svg
+ if (verify) {
+ console.debug(`metrics/compute/${login} > verify SVG`)
+ const libxmljs = (await import("libxmljs")).default
+ const parsed = libxmljs.parseXml(rendered)
+ if (parsed.errors.length)
+ throw new Error(`Malformed SVG : \n${parsed.errors.join("\n")}`)
+ }
+ }
+
+ //Result
+ console.debug(`metrics/compute/${login} > success`)
+ return {rendered, mime}
+ }
+ //Internal error
+ catch (error) {
+ //User not found
+ if (((Array.isArray(error.errors))&&(error.errors[0].type === "NOT_FOUND")))
+ throw new Error("user not found")
+ //Generic error
+ throw error
+ }
+ }
+
diff --git a/source/app/metrics/metadata.mjs b/source/app/metrics/metadata.mjs
new file mode 100644
index 00000000..3ba769a2
--- /dev/null
+++ b/source/app/metrics/metadata.mjs
@@ -0,0 +1,282 @@
+//Imports
+ import fs from "fs"
+ import path from "path"
+ import yaml from "js-yaml"
+ import url from "url"
+
+/** Metadata descriptor parser */
+ export default async function metadata({log = true} = {}) {
+ //Paths
+ const __metrics = path.join(path.dirname(url.fileURLToPath(import.meta.url)), "../../..")
+ const __templates = path.join(__metrics, "source/templates")
+ const __plugins = path.join(__metrics, "source/plugins")
+
+ //Init
+ const logger = log ? console.debug : () => null
+
+ //Load plugins metadata
+ let Plugins = {}
+ logger(`metrics/metadata > loading plugins metadata`)
+ for (const name of await fs.promises.readdir(__plugins)) {
+ if (!(await fs.promises.lstat(path.join(__plugins, name))).isDirectory())
+ continue
+ logger(`metrics/metadata > loading plugin metadata [${name}]`)
+ Plugins[name] = await metadata.plugin({__plugins, name, logger})
+ }
+ //Reorder keys
+ const {base, core, ...plugins} = Plugins
+ Plugins = {base, core, ...plugins}
+
+ //Load templates metadata
+ let Templates = {}
+ logger(`metrics/metadata > loading templates metadata`)
+ for (const name of await fs.promises.readdir(__templates)) {
+ if (!(await fs.promises.lstat(path.join(__templates, name))).isDirectory())
+ continue
+ if (/^@/.test(name))
+ continue
+ logger(`metrics/metadata > loading template metadata [${name}]`)
+ Templates[name] = await metadata.template({__templates, name, plugins, logger})
+ }
+ //Reorder keys
+ const {classic, repository, community, ...templates} = Templates
+ Templates = {classic, repository, ...templates, community}
+
+ //Metadata
+ return {plugins:Plugins, templates:Templates}
+ }
+
+/** Metadata extractor for templates */
+ metadata.plugin = async function ({__plugins, name, logger}) {
+ try {
+ //Load meta descriptor
+ const raw = `${await fs.promises.readFile(path.join(__plugins, name, "metadata.yml"), "utf-8")}`
+ const {inputs, ...meta} = yaml.load(raw)
+
+ //Inputs parser
+ {
+ meta.inputs = function ({data:{user = null} = {}, q, account}, defaults = {}) {
+ //Support check
+ if (!account)
+ logger(`metrics/inputs > account type not set for plugin ${name}!`)
+ if (account !== "bypass") {
+ if (!meta.supports?.includes(account))
+ throw {error:{message:`Not supported for: ${account}`, instance:new Error()}}
+ if ((q.repo)&&(!meta.supports?.includes("repository")))
+ throw {error:{message:`Not supported for: ${account} repositories`, instance:new Error()}}
+ }
+ //Inputs checks
+ const result = Object.fromEntries(Object.entries(inputs).map(([key, {type, format, default:defaulted, min, max, values}]) => [
+ //Format key
+ metadata.to.query(key, {name}),
+ //Format value
+ (defaulted => {
+ //Default value
+ let value = q[metadata.to.query(key)] ?? q[key] ?? defaulted
+ //Apply type conversion
+ switch (type) {
+ //Booleans
+ case "boolean":{
+ if (/^(?:[Tt]rue|[Oo]n|[Yy]es|1)$/.test(value))
+ return true
+ if (/^(?:[Ff]alse|[Oo]ff|[Nn]o|0)$/.test(value))
+ return false
+ return defaulted
+ }
+ //Numbers
+ case "number":{
+ value = Number(value)
+ if (!Number.isFinite(value))
+ value = defaulted
+ if (Number.isFinite(min))
+ value = Math.max(min, value)
+ if (Number.isFinite(max))
+ value = Math.min(value, max)
+ return value
+ }
+ //Array
+ case "array":{
+ try {
+ value = decodeURIComponent(value)
+ }
+ catch {
+ logger(`metrics/inputs > failed to decode uri : ${value}`)
+ value = defaulted
+ }
+ const separators = {"comma-separated":",", "space-separated":" "}
+ const separator = separators[[format].flat().filter(s => s in separators)[0]] ?? ","
+ return value.split(separator).map(v => v.trim().toLocaleLowerCase()).filter(v => Array.isArray(values) ? values.includes(v) : true).filter(v => v)
+ }
+ //String
+ case "string":{
+ value = value.trim()
+ if (user) {
+ if (value === ".user.login")
+ return user.login
+ if (value === ".user.twitter")
+ return user.twitterUsername
+ if (value === ".user.website")
+ return user.websiteUrl
+ }
+ if ((Array.isArray(values))&&(!values.includes(value)))
+ return defaulted
+ return value
+ }
+ //JSON
+ case "json":{
+ try {
+ value = JSON.parse(value)
+ }
+ catch {
+ logger(`metrics/inputs > failed to parse json : ${value}`)
+ value = JSON.parse(defaulted)
+ }
+ return value
+ }
+ //Token
+ case "token":{
+ return value
+ }
+ //Default
+ default:{
+ return value
+ }
+ }
+ })(defaults[key] ?? defaulted)
+ ]))
+ logger(`metrics/inputs > ${name} > ${JSON.stringify(result)}`)
+ return result
+ }
+ Object.assign(meta.inputs, inputs, Object.fromEntries(Object.entries(inputs).map(([key, value]) => [metadata.to.query(key, {name}), value])))
+ }
+
+ //Action metadata
+ {
+ //Extract comments
+ const comments = {}
+ raw.split(/(\r?\n){2,}/m)
+ .map(x => x.trim()).filter(x => x)
+ .map(x => x.split("\n").map(y => y.trim()).join("\n"))
+ .map(x => {
+ const input = x.match(new RegExp(`^\\s*(?${Object.keys(inputs).join("|")}):`, "m"))?.groups?.input ?? null
+ if (input)
+ comments[input] = x.match(new RegExp(`(?[\\s\\S]*?)(?=(?:${Object.keys(inputs).sort((a, b) => b.length - a.length).join("|")}):)`))?.groups?.comment
+ })
+
+ //Action descriptor
+ meta.action = Object.fromEntries(Object.entries(inputs).map(([key, value]) => [
+ key,
+ {
+ comment:comments[key] ?? "",
+ descriptor:yaml.dump({[key]:Object.fromEntries(Object.entries(value).filter(([key]) => ["description", "default", "required"].includes(key)))}, {quotingType:'"', noCompatMode:true})
+ }
+ ]))
+
+ //Action inputs
+ meta.inputs.action = function ({core}) {
+ //Build query object from inputs
+ const q = {}
+ for (const key of Object.keys(inputs)) {
+ const value = `${core.getInput(key)}`.trim()
+ try {
+ q[key] = decodeURIComponent(value)
+ }
+ catch {
+ logger(`metrics/inputs > failed to decode uri : ${value}`)
+ q[key] = value
+ }
+ }
+ return meta.inputs({q, account:"bypass"})
+ }
+ }
+
+ //Web metadata
+ {
+ meta.web = Object.fromEntries(Object.entries(inputs).map(([key, {type, description:text, example, default:defaulted, min = 0, max = 9999, values}]) => [
+ //Format key
+ metadata.to.query(key),
+ //Value descriptor
+ (() => {
+ switch (type) {
+ case "boolean":
+ return {text, type:"boolean"}
+ case "number":
+ return {text, type:"number", min, max, defaulted}
+ case "array":
+ return {text, type:"text", placeholder:example ?? defaulted, defaulted}
+ case "string":{
+ if (Array.isArray(values))
+ return {text, type:"select", values}
+ else
+ return {text, type:"text", placeholder:example ?? defaulted, defaulted}
+ }
+ case "json":
+ return {text, type:"text", placeholder:example ?? defaulted, defaulted}
+ default:
+ return null
+ }
+ })()
+ ]).filter(([key, value]) => (value)&&(key !== name)))
+ }
+
+ //Readme metadata
+ {
+ //Extract demos
+ const raw = `${await fs.promises.readFile(path.join(__plugins, name, "README.md"), "utf-8")}`
+ const demo = raw.match(/(?
+
+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)
+```
diff --git a/source/plugins/activity/index.mjs b/source/plugins/activity/index.mjs
index 691dd8c6..90d46370 100644
--- a/source/plugins/activity/index.mjs
+++ b/source/plugins/activity/index.mjs
@@ -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)
diff --git a/source/plugins/activity/metadata.yml b/source/plugins/activity/metadata.yml
new file mode 100644
index 00000000..7fe3eb48
--- /dev/null
+++ b/source/plugins/activity/metadata.yml
@@ -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
\ No newline at end of file
diff --git a/source/plugins/anilist/README.md b/source/plugins/anilist/README.md
new file mode 100644
index 00000000..56cce5fa
--- /dev/null
+++ b/source/plugins/anilist/README.md
@@ -0,0 +1,40 @@
+### ๐ธ Anilist ๐ง pre-release on @master
+
+The *anilist* plugin lets you display your favorites animes, mangas and characters from your [AniList](https://anilist.co) account.
+
+
+
+
+ Manga version
+
+
+ Favorites characters version
+
+
+
+
+
+
+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
+```
diff --git a/source/plugins/anilist/index.mjs b/source/plugins/anilist/index.mjs
index 3ff377e9..2b49ca65 100644
--- a/source/plugins/anilist/index.mjs
+++ b/source/plugins/anilist/index.mjs
@@ -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
}
diff --git a/source/plugins/anilist/metadata.yml b/source/plugins/anilist/metadata.yml
new file mode 100644
index 00000000..e5eface4
--- /dev/null
+++ b/source/plugins/anilist/metadata.yml
@@ -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
diff --git a/source/plugins/base/README.md b/source/plugins/base/README.md
new file mode 100644
index 00000000..a2c0c2b7
--- /dev/null
+++ b/source/plugins/base/README.md
@@ -0,0 +1,38 @@
+### ๐๏ธ Base content
+
+The *base* content is all metrics enabled by default.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+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
+```
diff --git a/source/plugins/base/index.mjs b/source/plugins/base/index.mjs
new file mode 100644
index 00000000..f339b118
--- /dev/null
+++ b/source/plugins/base/index.mjs
@@ -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},
+ })
+ }
+ }
\ No newline at end of file
diff --git a/source/plugins/base/metadata.yml b/source/plugins/base/metadata.yml
new file mode 100644
index 00000000..97bfd60b
--- /dev/null
+++ b/source/plugins/base/metadata.yml
@@ -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
diff --git a/source/queries/common.organization.graphql b/source/plugins/base/queries/organization.graphql
similarity index 94%
rename from source/queries/common.organization.graphql
rename to source/plugins/base/queries/organization.graphql
index c26b42b7..b9ce3679 100644
--- a/source/queries/common.organization.graphql
+++ b/source/plugins/base/queries/organization.graphql
@@ -1,4 +1,4 @@
-query MetricsOrganization {
+query BaseOrganization {
organization(login: "$login") {
databaseId
name
diff --git a/source/queries/repositories.graphql b/source/plugins/base/queries/repositories.graphql
similarity index 97%
rename from source/queries/repositories.graphql
rename to source/plugins/base/queries/repositories.graphql
index de5ed976..b2b4984c 100644
--- a/source/queries/repositories.graphql
+++ b/source/plugins/base/queries/repositories.graphql
@@ -1,4 +1,4 @@
-query Repositories {
+query BaseRepositories {
$account(login: "$login") {
repositories($after first: $repositories $forks, orderBy: {field: UPDATED_AT, direction: DESC}) {
edges {
diff --git a/source/queries/repository.graphql b/source/plugins/base/queries/repository.graphql
similarity index 97%
rename from source/queries/repository.graphql
rename to source/plugins/base/queries/repository.graphql
index c49a750b..a7567424 100644
--- a/source/queries/repository.graphql
+++ b/source/plugins/base/queries/repository.graphql
@@ -1,4 +1,4 @@
-query Repository {
+query BaseRepository {
$account(login: "$login") {
repository(name: "$repo") {
name
diff --git a/source/queries/common.graphql b/source/plugins/base/queries/user.graphql
similarity index 98%
rename from source/queries/common.graphql
rename to source/plugins/base/queries/user.graphql
index f7334c55..19eca20a 100644
--- a/source/queries/common.graphql
+++ b/source/plugins/base/queries/user.graphql
@@ -1,4 +1,4 @@
-query Metrics {
+query BaseUser {
user(login: "$login") {
databaseId
name
diff --git a/source/plugins/core/README.md b/source/plugins/core/README.md
new file mode 100644
index 00000000..22283723
--- /dev/null
+++ b/source/plugins/core/README.md
@@ -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
+```
diff --git a/source/templates/common.mjs b/source/plugins/core/index.mjs
similarity index 91%
rename from source/templates/common.mjs
rename to source/plugins/core/index.mjs
index 1ae99cba..3fd58af7 100644
--- a/source/templates/common.mjs
+++ b/source/plugins/core/index.mjs
@@ -1,5 +1,12 @@
-/** Template common processor */
- export default async function ({login, q, dflags}, {conf, data, rest, graphql, plugins, queries, account}, {s, pending, imports}) {
+/**
+ * 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}}
@@ -28,7 +35,6 @@
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`)
@@ -74,7 +80,7 @@
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${s(years)} ago` : months ? `${months} month${s(months)} ago` : `${Math.ceil(diff*365)} day${s(Math.ceil(diff*365))} ago`
+ 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(/(?\d{2}-\d{2})(?=T)/)?.groups?.mmdd).every((v, _, a) => v === a[0]) : false
//Compute calendar
@@ -118,4 +124,7 @@
return {name:"dflag.halloween", result:true}
})())
}
+
+ //Results
+ return null
}
\ No newline at end of file
diff --git a/source/plugins/core/metadata.yml b/source/plugins/core/metadata.yml
new file mode 100644
index 00000000..3b5db5fb
--- /dev/null
+++ b/source/plugins/core/metadata.yml
@@ -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
+ - /(?[-a-z0-9]+)[/](?[-a-z0-9]+)@(?[-a-z0-9]+):(?[-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: ""
\ No newline at end of file
diff --git a/source/plugins/followup/README.md b/source/plugins/followup/README.md
new file mode 100644
index 00000000..67ac1e8c
--- /dev/null
+++ b/source/plugins/followup/README.md
@@ -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.
+
+
+
+
+
+
+
+
+#### โน๏ธ Examples workflows
+
+[โก๏ธ Available options for this plugin](metadata.yml)
+
+```yaml
+- uses: lowlighter/metrics@latest
+ with:
+ # ... other options
+ plugin_followup: yes
+```
+
diff --git a/source/plugins/followup/index.mjs b/source/plugins/followup/index.mjs
index 2c29dd0d..03be73ad 100644
--- a/source/plugins/followup/index.mjs
+++ b/source/plugins/followup/index.mjs
@@ -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
}
diff --git a/source/plugins/followup/metadata.yml b/source/plugins/followup/metadata.yml
new file mode 100644
index 00000000..73e3e28f
--- /dev/null
+++ b/source/plugins/followup/metadata.yml
@@ -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
diff --git a/source/plugins/gists/README.md b/source/plugins/gists/README.md
new file mode 100644
index 00000000..d0326014
--- /dev/null
+++ b/source/plugins/gists/README.md
@@ -0,0 +1,21 @@
+### ๐ซ Gists
+
+The *gists* plugin displays your [gists](https://gist.github.com) metrics.
+
+
+
+
+
+
+
+
+#### โน๏ธ Examples workflows
+
+[โก๏ธ Available options for this plugin](metadata.yml)
+
+```yaml
+- uses: lowlighter/metrics@latest
+ with:
+ # ... other options
+ plugin_gists: yes
+```
diff --git a/source/plugins/gists/index.mjs b/source/plugins/gists/index.mjs
index 233fb49a..c1d5fa59 100644
--- a/source/plugins/gists/index.mjs
+++ b/source/plugins/gists/index.mjs
@@ -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}
}
diff --git a/source/plugins/gists/metadata.yml b/source/plugins/gists/metadata.yml
new file mode 100644
index 00000000..b7d7ee64
--- /dev/null
+++ b/source/plugins/gists/metadata.yml
@@ -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
diff --git a/source/queries/gists.graphql b/source/plugins/gists/queries/gists.graphql
similarity index 94%
rename from source/queries/gists.graphql
rename to source/plugins/gists/queries/gists.graphql
index a7b2dbac..876a8700 100644
--- a/source/queries/gists.graphql
+++ b/source/plugins/gists/queries/gists.graphql
@@ -1,4 +1,4 @@
-query Gists {
+query GistsDefault {
user(login: "$login") {
gists($after first: 100, orderBy: {field: UPDATED_AT, direction: DESC}) {
edges {
diff --git a/source/plugins/habits/README.md b/source/plugins/habits/README.md
new file mode 100644
index 00000000..125778fd
--- /dev/null
+++ b/source/plugins/habits/README.md
@@ -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.
+
+
+
+
+ Charts version
+
+
+
+
+
+
+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
+```
diff --git a/source/plugins/habits/index.mjs b/source/plugins/habits/index.mjs
index 5b6f8530..adede1a1 100644
--- a/source/plugins/habits/index.mjs
+++ b/source/plugins/habits/index.mjs
@@ -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
}
diff --git a/source/plugins/habits/metadata.yml b/source/plugins/habits/metadata.yml
new file mode 100644
index 00000000..8af6f2f4
--- /dev/null
+++ b/source/plugins/habits/metadata.yml
@@ -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
diff --git a/source/plugins/isocalendar/README.md b/source/plugins/isocalendar/README.md
new file mode 100644
index 00000000..b5db422c
--- /dev/null
+++ b/source/plugins/isocalendar/README.md
@@ -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.
+
+
+
+
+ Full year version
+
+
+
+
+
+
+#### โน๏ธ 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
+```
diff --git a/source/plugins/isocalendar/index.mjs b/source/plugins/isocalendar/index.mjs
index 35cf5403..2bc6b563 100644
--- a/source/plugins/isocalendar/index.mjs
+++ b/source/plugins/isocalendar/index.mjs
@@ -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 += ``
+
//Results
return {streak, max, average, svg, duration}
}
diff --git a/source/plugins/isocalendar/metadata.yml b/source/plugins/isocalendar/metadata.yml
new file mode 100644
index 00000000..914a5996
--- /dev/null
+++ b/source/plugins/isocalendar/metadata.yml
@@ -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
\ No newline at end of file
diff --git a/source/queries/calendar.graphql b/source/plugins/isocalendar/queries/calendar.graphql
similarity index 90%
rename from source/queries/calendar.graphql
rename to source/plugins/isocalendar/queries/calendar.graphql
index 91c47a8d..c47ad080 100644
--- a/source/queries/calendar.graphql
+++ b/source/plugins/isocalendar/queries/calendar.graphql
@@ -1,4 +1,4 @@
-query Calendar {
+query IsocalendarCalendar {
user(login: "$login") {
calendar:contributionsCollection(from: "$from", to: "$to") {
contributionCalendar {
diff --git a/source/plugins/languages/README.md b/source/plugins/languages/README.md
new file mode 100644
index 00000000..aca53e8f
--- /dev/null
+++ b/source/plugins/languages/README.md
@@ -0,0 +1,29 @@
+### ๐ท๏ธ Most used languages ๐ง plugin_languages_colors on @master
+
+The *languages* plugin displays which programming languages you use the most across all your repositories.
+
+
+
+
+
+
+
+
+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
+```
diff --git a/source/plugins/languages/index.mjs b/source/plugins/languages/index.mjs
index e7561f65..95a0261c 100644
--- a/source/plugins/languages/index.mjs
+++ b/source/plugins/languages/index.mjs
@@ -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
}
diff --git a/source/plugins/languages/metadata.yml b/source/plugins/languages/metadata.yml
new file mode 100644
index 00000000..bbbe6b1b
--- /dev/null
+++ b/source/plugins/languages/metadata.yml
@@ -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
+ - /((?[0-9])|(?[-+a-z0-9#])):(?#?[-a-z0-9]+)/
+ default: github
diff --git a/source/plugins/lines/README.md b/source/plugins/lines/README.md
new file mode 100644
index 00000000..1d798631
--- /dev/null
+++ b/source/plugins/lines/README.md
@@ -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.
+
+
+
+
+
+
+
+
+#### โน๏ธ Examples workflows
+
+[โก๏ธ Available options for this plugin](metadata.yml)
+
+```yaml
+- uses: lowlighter/metrics@latest
+ with:
+ # ... other options
+ plugin_lines: yes
+```
diff --git a/source/plugins/lines/index.mjs b/source/plugins/lines/index.mjs
index 15c448bc..ce7e5910 100644
--- a/source/plugins/lines/index.mjs
+++ b/source/plugins/lines/index.mjs
@@ -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
}
diff --git a/source/plugins/lines/metadata.yml b/source/plugins/lines/metadata.yml
new file mode 100644
index 00000000..f16df2d4
--- /dev/null
+++ b/source/plugins/lines/metadata.yml
@@ -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
diff --git a/source/plugins/music/README.md b/source/plugins/music/README.md
new file mode 100644
index 00000000..6f74b524
--- /dev/null
+++ b/source/plugins/music/README.md
@@ -0,0 +1,197 @@
+### ๐ผ Music plugin ๐ง lastfm on @master
+
+The *music* plugin lets you display :
+
+
+
+ ๐ผ Favorite tracks version
+
+
+ Recently listened version
+
+
+
+
+
+
+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.
+
+
+Apple Music
+
+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
+
+```
+
+And use this value in `plugin_music_playlist` option.
+
+
+
+
+Spotify
+
+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
+
+```
+
+And use this value in `plugin_music_playlist` option.
+
+
+
+
+Last.fm
+
+This mode is not supported for now.
+
+
+
+#### โน๏ธ 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.
+
+
+Apple Music
+
+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.
+
+
+
+
+Spotify
+
+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.
+
+
+
+
+Last.fm
+
+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.
+
+
+
+#### โน๏ธ 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 }}
+
+```
diff --git a/source/plugins/music/index.mjs b/source/plugins/music/index.mjs
index a0d487ce..e9f602a3 100644
--- a/source/plugins/music/index.mjs
+++ b/source/plugins/music/index.mjs
@@ -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)`}}
}
diff --git a/source/plugins/music/metadata.yml b/source/plugins/music/metadata.yml
new file mode 100644
index 00000000..365f282d
--- /dev/null
+++ b/source/plugins/music/metadata.yml
@@ -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
\ No newline at end of file
diff --git a/source/plugins/pagespeed/README.md b/source/plugins/pagespeed/README.md
new file mode 100644
index 00000000..96ca747c
--- /dev/null
+++ b/source/plugins/pagespeed/README.md
@@ -0,0 +1,40 @@
+
+### โฑ๏ธ Website performances
+
+The *pagespeed* plugin adds the performance statistics of the website attached on your account:
+
+
+
+
+ Detailed version
+
+
+ With screenshot version
+
+
+
+
+
+
+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)
+```
diff --git a/source/plugins/pagespeed/index.mjs b/source/plugins/pagespeed/index.mjs
index b8ef97b7..2f6f954e 100644
--- a/source/plugins/pagespeed/index.mjs
+++ b/source/plugins/pagespeed/index.mjs
@@ -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
}
diff --git a/source/plugins/pagespeed/metadata.yml b/source/plugins/pagespeed/metadata.yml
new file mode 100644
index 00000000..ffd3db21
--- /dev/null
+++ b/source/plugins/pagespeed/metadata.yml
@@ -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: ""
\ No newline at end of file
diff --git a/source/plugins/people/README.md b/source/plugins/people/README.md
new file mode 100644
index 00000000..d1c9eac4
--- /dev/null
+++ b/source/plugins/people/README.md
@@ -0,0 +1,52 @@
+### ๐งโ๐คโ๐ง People plugin ๐ง plugin_people_thanks, repository version and "sponsors" on @master
+
+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.
+
+
+
+
+ Followed people version
+
+
+ Special thanks version
+
+
+ Repository template version
+
+
+
+
+
+
+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
+```
diff --git a/source/plugins/people/index.mjs b/source/plugins/people/index.mjs
index 0b955654..65151883 100644
--- a/source/plugins/people/index.mjs
+++ b/source/plugins/people/index.mjs
@@ -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
diff --git a/source/plugins/people/metadata.yml b/source/plugins/people/metadata.yml
new file mode 100644
index 00000000..552e7b06
--- /dev/null
+++ b/source/plugins/people/metadata.yml
@@ -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
\ No newline at end of file
diff --git a/source/queries/people.graphql b/source/plugins/people/queries/people.graphql
similarity index 89%
rename from source/queries/people.graphql
rename to source/plugins/people/queries/people.graphql
index c69041eb..86a1c93a 100644
--- a/source/queries/people.graphql
+++ b/source/plugins/people/queries/people.graphql
@@ -1,4 +1,4 @@
-query People {
+query PeopleDefault {
user(login: "$login") {
login
$type($after first: 100) {
diff --git a/source/queries/people.repository.graphql b/source/plugins/people/queries/repository.graphql
similarity index 100%
rename from source/queries/people.repository.graphql
rename to source/plugins/people/queries/repository.graphql
diff --git a/source/queries/people.sponsors.graphql b/source/plugins/people/queries/sponsors.graphql
similarity index 100%
rename from source/queries/people.sponsors.graphql
rename to source/plugins/people/queries/sponsors.graphql
diff --git a/source/plugins/posts/README.md b/source/plugins/posts/README.md
new file mode 100644
index 00000000..e63ea6eb
--- /dev/null
+++ b/source/plugins/posts/README.md
@@ -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).
+
+
+
+
+
+
+
+
+#### โน๏ธ 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
+```
diff --git a/source/plugins/posts/index.mjs b/source/plugins/posts/index.mjs
index f31f7dce..aa85ef5f 100644
--- a/source/plugins/posts/index.mjs
+++ b/source/plugins/posts/index.mjs
@@ -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)`}}
}
diff --git a/source/plugins/posts/metadata.yml b/source/plugins/posts/metadata.yml
new file mode 100644
index 00000000..b52b1227
--- /dev/null
+++ b/source/plugins/posts/metadata.yml
@@ -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
+
diff --git a/source/plugins/projects/README.md b/source/plugins/projects/README.md
new file mode 100644
index 00000000..d23d5a07
--- /dev/null
+++ b/source/plugins/projects/README.md
@@ -0,0 +1,55 @@
+### ๐๏ธ Projects ๐ง plugin_projects_descriptions on @master
+
+ โ ๏ธ This plugin requires a personal token with public_repo scope.
+
+The *projects* plugin displays the progress of your profile projects.
+
+
+
+
+
+
+
+
+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).
+
+
+
+
+๐ฌ Create a personal project on GitHub
+
+On your profile, select the `Projects` tab:
+
+
+Fill the informations and set visibility to *public*:
+
+
+
+
+
+๐ฌ Use repositories projects
+
+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.
+
+
+
+
+
+#### โน๏ธ 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
+```
diff --git a/source/plugins/projects/index.mjs b/source/plugins/projects/index.mjs
index f735c27d..aa85e74a 100644
--- a/source/plugins/projects/index.mjs
+++ b/source/plugins/projects/index.mjs
@@ -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(/(?[-\w]+)[/](?[-\w]+)[/]projects[/](?\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}
}
diff --git a/source/plugins/projects/metadata.yml b/source/plugins/projects/metadata.yml
new file mode 100644
index 00000000..dd8963c7
--- /dev/null
+++ b/source/plugins/projects/metadata.yml
@@ -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
+ - /(?[-a-z0-9]+)[/](?[-a-z0-9]+)[/]projects[/](?[0-9]+)/
+ default: ""
+
+ # Display projects descriptions
+ plugin_projects_descriptions:
+ description: Display projects descriptions
+ type: boolean
+ default: no
\ No newline at end of file
diff --git a/source/queries/projects.repository.graphql b/source/plugins/projects/queries/repository.graphql
similarity index 90%
rename from source/queries/projects.repository.graphql
rename to source/plugins/projects/queries/repository.graphql
index 21bc9eef..d58ee7cd 100644
--- a/source/queries/projects.repository.graphql
+++ b/source/plugins/projects/queries/repository.graphql
@@ -1,4 +1,4 @@
-query RepositoryProject {
+query ProjectsRepository {
$account(login: "$user") {
repository(name: "$repository") {
project(number: $id) {
diff --git a/source/queries/projects.graphql b/source/plugins/projects/queries/user.graphql
similarity index 93%
rename from source/queries/projects.graphql
rename to source/plugins/projects/queries/user.graphql
index cad09502..a39be1fa 100644
--- a/source/queries/projects.graphql
+++ b/source/plugins/projects/queries/user.graphql
@@ -1,4 +1,4 @@
-query Projects {
+query ProjectsUser {
$account(login: "$login") {
projects(last: $limit, states: OPEN, orderBy: {field: UPDATED_AT, direction: DESC}) {
totalCount
diff --git a/source/plugins/stargazers/README.md b/source/plugins/stargazers/README.md
new file mode 100644
index 00000000..7f4ded41
--- /dev/null
+++ b/source/plugins/stargazers/README.md
@@ -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.
+
+
+
+
+
+
+
+
+#### โน๏ธ Examples workflows
+
+[โก๏ธ Available options for this plugin](metadata.yml)
+
+```yaml
+- uses: lowlighter/metrics@latest
+ with:
+ # ... other options
+ plugin_stargazers: yes
+```
diff --git a/source/plugins/stargazers/index.mjs b/source/plugins/stargazers/index.mjs
index 65e39b03..1d2ff4ea 100644
--- a/source/plugins/stargazers/index.mjs
+++ b/source/plugins/stargazers/index.mjs
@@ -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}
}
diff --git a/source/plugins/stargazers/metadata.yml b/source/plugins/stargazers/metadata.yml
new file mode 100644
index 00000000..6488d416
--- /dev/null
+++ b/source/plugins/stargazers/metadata.yml
@@ -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
diff --git a/source/queries/stargazers.graphql b/source/plugins/stargazers/queries/stargazers.graphql
similarity index 88%
rename from source/queries/stargazers.graphql
rename to source/plugins/stargazers/queries/stargazers.graphql
index fed1601a..dd1be4c7 100644
--- a/source/queries/stargazers.graphql
+++ b/source/plugins/stargazers/queries/stargazers.graphql
@@ -1,4 +1,4 @@
-query Stargazers {
+query StargazersDefault {
repository(name: "$repository", owner: "$login") {
stargazers($after first: 100, orderBy: {field: STARRED_AT, direction: ASC}) {
edges {
diff --git a/source/plugins/stars/README.md b/source/plugins/stars/README.md
new file mode 100644
index 00000000..f57a2a3c
--- /dev/null
+++ b/source/plugins/stars/README.md
@@ -0,0 +1,22 @@
+### ๐ Recently starred repositories
+
+The *stars* plugin displays your recently starred repositories.
+
+
+
+
+
+
+
+
+#### โน๏ธ 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
+```
diff --git a/source/plugins/stars/index.mjs b/source/plugins/stars/index.mjs
index fa4c1d3e..c3aa6ebd 100644
--- a/source/plugins/stars/index.mjs
+++ b/source/plugins/stars/index.mjs
@@ -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}
}
diff --git a/source/plugins/stars/metadata.yml b/source/plugins/stars/metadata.yml
new file mode 100644
index 00000000..68bf5007
--- /dev/null
+++ b/source/plugins/stars/metadata.yml
@@ -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
\ No newline at end of file
diff --git a/source/queries/starred.graphql b/source/plugins/stars/queries/stars.graphql
similarity index 96%
rename from source/queries/starred.graphql
rename to source/plugins/stars/queries/stars.graphql
index 35e5c137..66d64e5e 100644
--- a/source/queries/starred.graphql
+++ b/source/plugins/stars/queries/stars.graphql
@@ -1,4 +1,4 @@
-query Starred {
+query StarsDefault {
user(login: "$login") {
starredRepositories(first: $limit, orderBy: {field: STARRED_AT, direction: DESC}) {
edges {
diff --git a/source/plugins/topics/README.md b/source/plugins/topics/README.md
new file mode 100644
index 00000000..c231fd14
--- /dev/null
+++ b/source/plugins/topics/README.md
@@ -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.
+
+
+
+
+ Mastered and known technologies version
+
+
+
+
+
+
+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
+```
diff --git a/source/plugins/topics/index.mjs b/source/plugins/topics/index.mjs
index f3b6ceaa..8eaa1488 100644
--- a/source/plugins/topics/index.mjs
+++ b/source/plugins/topics/index.mjs
@@ -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}
}
diff --git a/source/plugins/topics/metadata.yml b/source/plugins/topics/metadata.yml
new file mode 100644
index 00000000..dca0e6d6
--- /dev/null
+++ b/source/plugins/topics/metadata.yml
@@ -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
\ No newline at end of file
diff --git a/source/plugins/traffic/README.md b/source/plugins/traffic/README.md
new file mode 100644
index 00000000..bf4fb3cc
--- /dev/null
+++ b/source/plugins/traffic/README.md
@@ -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.
+
+
+
+
+
+
+
+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
+```
diff --git a/source/plugins/traffic/index.mjs b/source/plugins/traffic/index.mjs
index 5d78bd64..ff894d2f 100644
--- a/source/plugins/traffic/index.mjs
+++ b/source/plugins/traffic/index.mjs
@@ -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}
}
diff --git a/source/plugins/traffic/metadata.yml b/source/plugins/traffic/metadata.yml
new file mode 100644
index 00000000..0ba5cdaf
--- /dev/null
+++ b/source/plugins/traffic/metadata.yml
@@ -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
diff --git a/source/plugins/tweets/README.md b/source/plugins/tweets/README.md
new file mode 100644
index 00000000..2377cdb1
--- /dev/null
+++ b/source/plugins/tweets/README.md
@@ -0,0 +1,36 @@
+### ๐ค Tweets
+
+The recent *tweets* plugin displays your latest tweets from your [Twitter](https://twitter.com) account.
+
+
+
+
+
+
+
+
+
+๐ฌ Obtaining a Twitter token
+
+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.
+
+
+
+
+
+#### โน๏ธ 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
+```
diff --git a/source/plugins/tweets/index.mjs b/source/plugins/tweets/index.mjs
index 429ad3b1..98447394 100644
--- a/source/plugins/tweets/index.mjs
+++ b/source/plugins/tweets/index.mjs
@@ -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, ` $1 `)
, {"&":true})
}))
+
//Result
return {username, profile, list:tweets}
}
diff --git a/source/plugins/tweets/metadata.yml b/source/plugins/tweets/metadata.yml
new file mode 100644
index 00000000..8594d9cc
--- /dev/null
+++ b/source/plugins/tweets/metadata.yml
@@ -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
diff --git a/source/templates/README.md b/source/templates/README.md
new file mode 100644
index 00000000..39ec53cb
--- /dev/null
+++ b/source/templates/README.md
@@ -0,0 +1,9 @@
+## ๐ผ๏ธ Templates
+
+Templates lets you change general appearance of rendered metrics.
+See their respective documentation for more informations about how to setup them:
+
+* [๐ Classic](/source/templates/classic/README.md)
+* [๐ Repository](/source/templates/repository/README.md)
+* [๐ Terminal](/source/templates/terminal/README.md)
+* [๐ Community templates](/source/templates/community/README.md)
diff --git a/source/templates/classic/README.md b/source/templates/classic/README.md
new file mode 100644
index 00000000..80759f2d
--- /dev/null
+++ b/source/templates/classic/README.md
@@ -0,0 +1,19 @@
+### ๐ Classic
+
+Default template, mimicking GitHub visual identity.
+
+
+
+
+
+
+
+
+#### โน๏ธ Examples workflows
+
+```yaml
+- uses: lowlighter/metrics@latest
+ with:
+ # ... other options
+ template: classic
+```
diff --git a/source/templates/classic/partials/lines.ejs b/source/templates/classic/partials/lines.ejs
new file mode 100644
index 00000000..ca606c7e
--- /dev/null
+++ b/source/templates/classic/partials/lines.ejs
@@ -0,0 +1 @@
+<%# Included in base.repositories.ejs %>
\ No newline at end of file
diff --git a/source/templates/classic/partials/traffic.ejs b/source/templates/classic/partials/traffic.ejs
new file mode 100644
index 00000000..ca606c7e
--- /dev/null
+++ b/source/templates/classic/partials/traffic.ejs
@@ -0,0 +1 @@
+<%# Included in base.repositories.ejs %>
\ No newline at end of file
diff --git a/source/templates/classic/template.mjs b/source/templates/classic/template.mjs
index 42070d46..515544fa 100644
--- a/source/templates/classic/template.mjs
+++ b/source/templates/classic/template.mjs
@@ -1,8 +1,5 @@
-//Imports
- import common from "./../common.mjs"
-
/** Template processor */
export default async function ({login, q}, {conf, data, rest, graphql, plugins, queries}, {s, pending, imports}) {
- //Common
- await common(...arguments)
+ //Core
+ await imports.plugins.core(...arguments)
}
\ No newline at end of file
diff --git a/source/templates/community/README.md b/source/templates/community/README.md
new file mode 100644
index 00000000..c07d2c30
--- /dev/null
+++ b/source/templates/community/README.md
@@ -0,0 +1,25 @@
+### ๐ Community templates
+
+It is possible to use official releases with templates from forked repositories (whether you own them or not).
+
+Use `setup_community_templates` option to specify additional external sources using following format: `user/repo@branch:template`.
+Templates added this way will be downloaded through git and can be used by prefixing their name with an `@`.
+
+By default, community templates use `template.mjs` from official `classic` template instead of their own, to prevent executing malicious code and avoid token leaks.
+
+If you trust it, append `+trust` after their name.
+
+```yaml
+- uses: lowlighter/metrics@master
+ with:
+ # ... other options
+ template: "@super-metrics"
+ # Download "super-metrics" and "trusted-metrics" templates from "octocat/metrics@master"
+ # "@trusted-metrics" template can execute remote JavaScript code
+ setup_community_templates: octocat/metrics@master:super-metrics, octocat/metrics@master:trusted-metrics+trust
+```
+
+To create a new community template, fork this repository and create a new folder in `/source/templates` with same structure as current templates.
+Then, it's just as simple as HTML and CSS with a bit of JavaScript!
+
+If you made something awesome, please share it here!
\ No newline at end of file
diff --git a/source/templates/repository/README.md b/source/templates/repository/README.md
new file mode 100644
index 00000000..8adbafba
--- /dev/null
+++ b/source/templates/repository/README.md
@@ -0,0 +1,21 @@
+### ๐ Repository
+
+Template crafted for repositories, mimicking GitHub visual identity.
+
+
+
+
+
+
+
+
+#### โน๏ธ Examples workflows
+
+```yaml
+- uses: lowlighter/metrics@latest
+ with:
+ # ... other options
+ template: classic
+ user: repository-owner # Optional if you're the owner of target repository
+ query: '{"repo":"repository-name"}' # Use a JSON encoded object to pass your repository name in "repo" key
+```
diff --git a/source/templates/repository/partials/lines.ejs b/source/templates/repository/partials/lines.ejs
new file mode 100644
index 00000000..ca606c7e
--- /dev/null
+++ b/source/templates/repository/partials/lines.ejs
@@ -0,0 +1 @@
+<%# Included in base.repositories.ejs %>
\ No newline at end of file
diff --git a/source/templates/repository/partials/traffic.ejs b/source/templates/repository/partials/traffic.ejs
new file mode 100644
index 00000000..ca606c7e
--- /dev/null
+++ b/source/templates/repository/partials/traffic.ejs
@@ -0,0 +1 @@
+<%# Included in base.repositories.ejs %>
\ No newline at end of file
diff --git a/source/templates/repository/template.mjs b/source/templates/repository/template.mjs
index e23fd3a2..b4e39c59 100644
--- a/source/templates/repository/template.mjs
+++ b/source/templates/repository/template.mjs
@@ -1,6 +1,3 @@
-//Imports
- import common from "./../common.mjs"
-
/** Template processor */
export default async function ({login, q}, {conf, data, rest, graphql, plugins, queries, account}, {s, pending, imports}) {
//Check arguments
@@ -8,13 +5,13 @@
if (!repo) {
console.debug(`metrics/compute/${login}/${repo} > error, repo was undefined`)
data.errors.push({error:{message:`You must pass a "repo" argument to use this template`}})
- return await common(...arguments)
+ return await imports.plugins.core(...arguments)
}
console.debug(`metrics/compute/${login}/${repo} > switching to mode ${account}`)
//Retrieving single repository
console.debug(`metrics/compute/${login}/${repo} > retrieving single repository ${repo}`)
- const {[account]:{repository}} = await graphql(queries.repository({login, repo, account}))
+ const {[account]:{repository}} = await graphql(queries.base.repository({login, repo, account}))
data.user.repositories.nodes = [repository]
data.repo = repository
@@ -66,8 +63,8 @@
//Override plugins parameters
q["projects.limit"] = 0
- //Common
- await common(...arguments)
+ //Core
+ await imports.plugins.core(...arguments)
await Promise.all(pending)
//Set repository name
diff --git a/source/templates/terminal/README.md b/source/templates/terminal/README.md
new file mode 100644
index 00000000..7b25ec42
--- /dev/null
+++ b/source/templates/terminal/README.md
@@ -0,0 +1,19 @@
+### ๐ Terminal
+
+Terminal template, mimicking a SSH session.
+
+