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