diff --git a/source/app/metrics/metadata.mjs b/source/app/metrics/metadata.mjs
index 5b3db5c8..f21ddc57 100644
--- a/source/app/metrics/metadata.mjs
+++ b/source/app/metrics/metadata.mjs
@@ -84,12 +84,16 @@ export default async function metadata({log = true, diff = false} = {}) {
return {plugins:Plugins, templates:Templates, packaged, descriptor}
}
+/**Metadata extractor for inputs */
+metadata.inputs = {}
+
/**Metadata extractor for templates */
metadata.plugin = async function({__plugins, __templates, 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)
+ Object.assign(metadata.inputs, inputs)
//category
if (!categories.includes(meta.category))
@@ -345,6 +349,8 @@ metadata.plugin = async function({__plugins, __templates, name, logger}) {
cell.push(`⏩ Inherits ${o.inherits}
`)
if (o.global)
cell.push("⏭️ Global option
")
+ if (/^(?:[Ff]alse|[Oo]ff|[Nn]o|0)$/.test(o.preset))
+ cell.push("⏯️ Cannot be preset
")
if (o.testing)
cell.push("🔧 For development
")
if (!Object.keys(previous?.inputs ?? {}).includes(option))
diff --git a/source/app/metrics/presets.mjs b/source/app/metrics/presets.mjs
new file mode 100644
index 00000000..98dbd371
--- /dev/null
+++ b/source/app/metrics/presets.mjs
@@ -0,0 +1,70 @@
+//Imports
+import fs from "fs/promises"
+import yaml from "js-yaml"
+import fetch from "node-fetch"
+import metadata from "./metadata.mjs"
+
+/**Presets parser */
+export default async function presets(list, {log = true, core = null} = {}) {
+ //Init
+ const {plugins} = await metadata({log:false})
+ const {"config.presets":files} = plugins.core.inputs({q:{"config.presets":list}, account:"bypass"})
+ const logger = log ? console.debug : () => null
+ const allowed = Object.entries(metadata.inputs).filter(([_, {type, preset}]) => (type !== "token")&&(!/^(?:[Ff]alse|[Oo]ff|[Nn]o|0)$/.test(preset))).map(([key]) => key)
+ const env = core ? "action" : "web"
+ const options = {}
+
+ //Load presets
+ for (const file of files) {
+ try {
+ //Load and parse preset
+ logger(`metrics/presets > loading ${file}`)
+ let text = ""
+ if (file.startsWith("@")) {
+ logger(`metrics/presets > ${file} seems to be predefined preset, fetching`)
+ text = await fetch(`https://raw.githubusercontent.com/lowlighter/metrics/presets/${file.substring(1)}/preset.yaml`).then(response => response.text())
+ }
+ else if (file.startsWith("https://")) {
+ logger(`metrics/presets > ${file} seems to be an url, fetching`)
+ text = await fetch(file).then(response => response.text())
+ }
+ else if (env === "action") {
+ logger(`metrics/presets > ${file} seems to be a local file, reading`)
+ text = `${await fs.readFile(file)}`
+ }
+ else {
+ logger(`metrics/presets > ${file} cannot be loaded in current environment ${env}, skipping`)
+ continue
+ }
+ const {schema, with:inputs} = yaml.load(text)
+ logger(`metrics/presets > ${file} preset schema is ${schema}`)
+
+ //Evaluate preset
+ switch (`${schema}`) {
+ case "draft":{
+ for (let [key, value] of Object.entries(inputs)) {
+ if (!allowed.includes(key)) {
+ logger(`metrics/presets > ${key} is specified but is not allowed in preset, skipping`)
+ continue
+ }
+ if (env === "web")
+ key = metadata.to.query(key)
+ if (key in options)
+ logger(`metrics/presets > ${key} was already specified by another preset, overwriting`)
+ options[key] = value
+ }
+ break
+ }
+ default:
+ throw new Error(`unsupported preset schema: ${schema}`)
+ }
+ }
+ //Handle errors
+ catch (error) {
+ if (env === "action")
+ console.log(`::warning::skipping preset ${file}: ${error.message}`)
+ logger(`metrics/presets > an error occured while loading preset ${file} (${error}), ignoring`)
+ }
+ }
+ return options
+}
diff --git a/source/app/web/instance.mjs b/source/app/web/instance.mjs
index d40e90f0..529e7f6f 100644
--- a/source/app/web/instance.mjs
+++ b/source/app/web/instance.mjs
@@ -8,6 +8,7 @@ import cache from "memory-cache"
import util from "util"
import mocks from "../../../tests/mocks/index.mjs"
import metrics from "../metrics/index.mjs"
+import presets from "../metrics/presets.mjs"
import setup from "../metrics/setup.mjs"
/**App */
@@ -252,6 +253,10 @@ export default async function({mock, nosettings} = {}) {
//Render
const q = req.query
console.debug(`metrics/app/${login} > ${util.inspect(q, {depth:Infinity, maxStringLength:256})}`)
+ if ((q["config.presets"])&&(conf.settings.extras?.presets ?? conf.settings.extras?.default ?? false)) {
+ console.debug(`metrics/app/${login} > presets have been specified, loading them`)
+ Object.assign(q, await presets(q["config.presets"]))
+ }
const {rendered, mime} = await metrics({login, q}, {
graphql,
rest,
diff --git a/source/app/web/settings.example.json b/source/app/web/settings.example.json
index 5ce7b907..032ff5ff 100644
--- a/source/app/web/settings.example.json
+++ b/source/app/web/settings.example.json
@@ -27,6 +27,7 @@
},
"extras": {
"default": false, "//": "Default extras state (advised to let 'false' unless in debug mode)",
+ "presets": false, "//": "Allow use of 'config.presets' option",
"css": false, "//": "Allow use of 'extras.css' option",
"js": false, "//": "Allow use of 'extras.js' option",
"features": false, "//": "Enable extra features (advised to let 'false' on web instances)"
diff --git a/source/plugins/core/README.md b/source/plugins/core/README.md
index 4f47609b..646f5938 100644
--- a/source/plugins/core/README.md
+++ b/source/plugins/core/README.md
@@ -53,6 +53,38 @@ Content can be manually ordered using `config_order` option.
> 💡 Omitted sections will be appended at the end using default order
+## 🪛 Using presets
+
+> 🚧 This feature is an early implementation and may change before official release
+
+It is possible to reuse the same configuration across different repositories and workflows using configuration presets.
+A preset override the default values of inputs, and multiple presets can be provided at once through URLs or file paths.
+
+Options resolution is done in the following order:
+- default values
+- presets, from first to last
+- user values
+
+*Example: using a configuration preset from an url*
+```yaml
+- uses: lowlighter/metrics@latest
+ with:
+ config_presets: https://raw.githubusercontent.com/lowlighter/metrics/presets/lunar-red/preset.yaml
+```
+
+Some presets are hosted on this repository on the [`@presets`](https://github.com/lowlighter/metrics/tree/presets) branch and can be used directly by using using their identifier prefixed by an arobase (`@`).
+
+*Example: using a pre-defined configuration preset*
+```yaml
+- uses: lowlighter/metrics@latest
+ with:
+ config_presets: "@lunar-red"
+```
+
+> ⚠️ `🔐 Tokens` and options marked with `⏯️ Cannot be preset`, as they suggest, cannot be preset and thus requires to be explicitely defined to be set.
+
+> ℹ️ Presets configurations use [schemas](https://github.com/lowlighter/metrics/tree/presets/%40schema) to ensure compatibility between format changes
+
## 🎨 Custom CSS styling
Additional CSS can be injected using `extras_css` option.
diff --git a/source/plugins/core/examples.yml b/source/plugins/core/examples.yml
index c84f6430..8f2d71c7 100644
--- a/source/plugins/core/examples.yml
+++ b/source/plugins/core/examples.yml
@@ -29,6 +29,19 @@
token: ${{ secrets.METRICS_TOKEN }}
config_output: png
+- name: Presets
+ uses: lowlighter/metrics@latest
+ with:
+ filename: metrics.presets.svg
+ token: ${{ secrets.METRICS_TOKEN }}
+ base: header, repositories
+ config_presets: https://raw.githubusercontent.com/lowlighter/metrics/presets/lunar-red/preset.yaml
+ prod:
+ skip: true
+ test:
+ modes:
+ - web
+
- name: Plugin error example
uses: lowlighter/metrics@latest
with:
diff --git a/source/plugins/core/metadata.yml b/source/plugins/core/metadata.yml
index 817373fe..e34e0d0c 100644
--- a/source/plugins/core/metadata.yml
+++ b/source/plugins/core/metadata.yml
@@ -25,6 +25,7 @@ inputs:
Defaults to `token` owner username.
type: string
default: ""
+ preset: no
repo:
description: |
@@ -33,6 +34,7 @@ inputs:
This option is revevalant only for repositories templates
type: string
default: ""
+ preset: no
committer_token:
description: |
@@ -67,6 +69,7 @@ inputs:
Specify an existing gist id (can be retrieved from its URL) when using `output_action: gist`.
type: string
default: ""
+ preset: no
filename:
description: |
@@ -307,6 +310,14 @@ inputs:
- markdown-pdf
- insights
+ config_presets:
+ description: Configuration presets
+ type: array
+ format: comma-separated
+ default: ""
+ preset: no
+ example: "@lunar-red"
+
retries:
description: Retries in case of failures (for rendering)
type: number
@@ -357,6 +368,7 @@ inputs:
type: boolean
default: yes
testing: yes
+ preset: no
plugins_errors_fatal:
description: |
@@ -366,6 +378,7 @@ inputs:
type: boolean
default: no
testing: yes
+ preset: no
debug:
description: |
@@ -375,12 +388,14 @@ inputs:
type: boolean
default: no
testing: yes
+ preset: no
verify:
description: SVG validity check
type: boolean
default: no
testing: yes
+ preset: no
debug_flags:
description: |
@@ -398,6 +413,7 @@ inputs:
- --halloween
- --error
testing: yes
+ preset: no
dryrun:
description: |
@@ -407,6 +423,7 @@ inputs:
type: boolean
default: no
testing: yes
+ preset: no
experimental_features:
description: |
@@ -419,9 +436,11 @@ inputs:
values:
- --optimize-svg
testing: yes
+ preset: no
use_mocked_data:
description: Use mocked data instead of live APIs
type: boolean
default: no
testing: yes
+ preset: no