From a368636ec43003da27de392fff2ae6d45b1eaf50 Mon Sep 17 00:00:00 2001 From: Gronod Date: Sun, 17 May 2026 18:36:16 +0100 Subject: [PATCH 1/7] ci: add separate docs-check workflow for Markdown lint and Mermaid parse validation - docs-check.yml runs on push/PR only when .md files change - markdown-lint job: uses markdownlint-cli to check all .md files - mermaid-parse job: extracts all mermaid blocks from .md files and validates each via mmdc (mermaid-js CLI) in headless Chromium - Both jobs use continue-on-error: true so docs failures never block a release or fail the main CI pipeline - .markdownlint.json disables MD013 (line length), MD033 (inline HTML), MD041 (first-line heading) to reduce noise on this repo --- .gitea/workflows/docs-check.yml | 109 ++++++++++++++++++++++++++++++++ .markdownlint.json | 6 ++ 2 files changed, 115 insertions(+) create mode 100644 .gitea/workflows/docs-check.yml create mode 100644 .markdownlint.json diff --git a/.gitea/workflows/docs-check.yml b/.gitea/workflows/docs-check.yml new file mode 100644 index 0000000..4e62b34 --- /dev/null +++ b/.gitea/workflows/docs-check.yml @@ -0,0 +1,109 @@ +name: Docs Check + +on: + push: + branches: ["**"] + paths: + - "**.md" + - ".gitea/workflows/docs-check.yml" + pull_request: + branches: ["**"] + paths: + - "**.md" + - ".gitea/workflows/docs-check.yml" + +jobs: + markdown-lint: + name: Markdown lint + runs-on: ubuntu-latest + continue-on-error: true + steps: + - uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: "22" + + - name: Install markdownlint-cli + run: npm install -g markdownlint-cli + + - name: Lint all Markdown files + run: markdownlint "**/*.md" --ignore node_modules + + mermaid-parse: + name: Mermaid diagram parse check + runs-on: ubuntu-latest + continue-on-error: true + steps: + - uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: "22" + + - name: Install mermaid-js CLI + run: npm install -g @mermaid-js/mermaid-cli + + - name: Write Puppeteer config (no-sandbox for CI) + run: | + echo '{"args":["--no-sandbox","--disable-setuid-sandbox"]}' > /tmp/puppeteer-config.json + + - name: Extract and validate Mermaid diagrams + run: | + node - <<'EOF' + const fs = require('fs'); + const path = require('path'); + const { execSync } = require('child_process'); + const os = require('os'); + + function findMarkdownFiles(dir) { + const results = []; + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + const full = path.join(dir, entry.name); + if (entry.isDirectory() && entry.name !== 'node_modules' && !entry.name.startsWith('.')) { + results.push(...findMarkdownFiles(full)); + } else if (entry.isFile() && entry.name.endsWith('.md')) { + results.push(full); + } + } + return results; + } + + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mermaid-')); + let errors = 0; + let total = 0; + + for (const mdFile of findMarkdownFiles('.')) { + const content = fs.readFileSync(mdFile, 'utf8'); + const blocks = [...content.matchAll(/^```mermaid\n([\s\S]*?)^```/gm)]; + if (!blocks.length) continue; + console.log(`\nChecking ${mdFile} (${blocks.length} diagram(s))`); + + blocks.forEach((match, i) => { + total++; + const mmdFile = path.join(tmpDir, `diagram-${total}.mmd`); + const svgFile = path.join(tmpDir, `diagram-${total}.svg`); + fs.writeFileSync(mmdFile, match[1]); + try { + execSync( + `mmdc -i "${mmdFile}" -o "${svgFile}" --puppeteerConfigFile /tmp/puppeteer-config.json`, + { stdio: 'pipe' } + ); + console.log(` [OK] diagram ${i + 1}`); + } catch (err) { + const msg = (err.stderr || err.stdout || '').toString().split('\n')[0]; + console.error(` [FAIL] diagram ${i + 1}: ${msg}`); + console.log(`::warning file=${mdFile}::Mermaid diagram ${i + 1} failed: ${msg}`); + errors++; + } + }); + } + + console.log(`\nTotal diagrams checked: ${total}. Failures: ${errors}`); + if (errors > 0) { + console.log(`::warning::${errors} Mermaid diagram(s) failed to parse.`); + process.exit(1); + } + EOF diff --git a/.markdownlint.json b/.markdownlint.json new file mode 100644 index 0000000..1dd8622 --- /dev/null +++ b/.markdownlint.json @@ -0,0 +1,6 @@ +{ + "default": true, + "MD013": false, + "MD033": false, + "MD041": false +} From a42392fec65aff63e63b41d8d3d4a60149268500 Mon Sep 17 00:00:00 2001 From: Gronod Date: Sun, 17 May 2026 18:36:58 +0100 Subject: [PATCH 2/7] ci: trigger docs-check workflow --- README.md | 1 + docs/ARCHITECTURE.md | 1 + 2 files changed, 2 insertions(+) diff --git a/README.md b/README.md index 3e60666..a825280 100644 --- a/README.md +++ b/README.md @@ -342,3 +342,4 @@ MIT --- *sofarr: See what has downloaded "so far" from the comfort of your "sofa"* + diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index e566804..86b04d6 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -1469,3 +1469,4 @@ flowchart TD style AF fill:#d4edda style AG fill:#f8d7da ``` + From 6bf8098265e88be68f0d7388cb3beb07e738e8d5 Mon Sep 17 00:00:00 2001 From: Gronod Date: Sun, 17 May 2026 18:40:51 +0100 Subject: [PATCH 3/7] ci: disable noisy markdownlint rules (table style, blanks, code lang, etc) --- .markdownlint.json | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/.markdownlint.json b/.markdownlint.json index 1dd8622..0546a3e 100644 --- a/.markdownlint.json +++ b/.markdownlint.json @@ -1,6 +1,17 @@ { "default": true, + "MD009": false, + "MD012": false, "MD013": false, + "MD022": false, + "MD029": false, + "MD031": false, + "MD032": false, "MD033": false, - "MD041": false + "MD034": false, + "MD036": false, + "MD040": false, + "MD041": false, + "MD058": false, + "MD060": false } From 6cf01f55300ca8570f7869b7f2ca47da21e1f648 Mon Sep 17 00:00:00 2001 From: Gronod Date: Sun, 17 May 2026 18:50:46 +0100 Subject: [PATCH 4/7] =?UTF-8?q?ci:=20fix=20mermaid=20parse=20check=20?= =?UTF-8?q?=E2=80=94=20use=20mermaid.core.mjs=20(no=20Puppeteer/Chromium?= =?UTF-8?q?=20needed)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/workflows/docs-check.yml | 67 +++++++++++++-------------------- 1 file changed, 27 insertions(+), 40 deletions(-) diff --git a/.gitea/workflows/docs-check.yml b/.gitea/workflows/docs-check.yml index 4e62b34..f05352b 100644 --- a/.gitea/workflows/docs-check.yml +++ b/.gitea/workflows/docs-check.yml @@ -43,67 +43,54 @@ jobs: with: node-version: "22" - - name: Install mermaid-js CLI - run: npm install -g @mermaid-js/mermaid-cli - - - name: Write Puppeteer config (no-sandbox for CI) - run: | - echo '{"args":["--no-sandbox","--disable-setuid-sandbox"]}' > /tmp/puppeteer-config.json + - name: Install mermaid + run: npm install mermaid - name: Extract and validate Mermaid diagrams run: | - node - <<'EOF' - const fs = require('fs'); - const path = require('path'); - const { execSync } = require('child_process'); - const os = require('os'); + cat > check-mermaid.mjs << 'SCRIPT' + import { readFileSync, readdirSync } from 'fs'; + import { join } from 'path'; + import mermaid from './node_modules/mermaid/dist/mermaid.core.mjs'; - function findMarkdownFiles(dir) { - const results = []; - for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { - const full = path.join(dir, entry.name); - if (entry.isDirectory() && entry.name !== 'node_modules' && !entry.name.startsWith('.')) { - results.push(...findMarkdownFiles(full)); - } else if (entry.isFile() && entry.name.endsWith('.md')) { - results.push(full); - } + function findMdFiles(dir) { + const out = []; + for (const e of readdirSync(dir, { withFileTypes: true })) { + const full = join(dir, e.name); + if (e.isDirectory() && e.name !== 'node_modules' && !e.name.startsWith('.')) + out.push(...findMdFiles(full)); + else if (e.isFile() && e.name.endsWith('.md')) + out.push(full); } - return results; + return out; } - const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mermaid-')); - let errors = 0; - let total = 0; + let errors = 0, total = 0; - for (const mdFile of findMarkdownFiles('.')) { - const content = fs.readFileSync(mdFile, 'utf8'); - const blocks = [...content.matchAll(/^```mermaid\n([\s\S]*?)^```/gm)]; + for (const mdFile of findMdFiles('.')) { + const content = readFileSync(mdFile, 'utf8'); + const blocks = [...content.matchAll(/^```mermaid\n([\s\S]*?)^```/gm)]; if (!blocks.length) continue; console.log(`\nChecking ${mdFile} (${blocks.length} diagram(s))`); - - blocks.forEach((match, i) => { + for (let i = 0; i < blocks.length; i++) { total++; - const mmdFile = path.join(tmpDir, `diagram-${total}.mmd`); - const svgFile = path.join(tmpDir, `diagram-${total}.svg`); - fs.writeFileSync(mmdFile, match[1]); + const diagram = blocks[i][1].trim(); try { - execSync( - `mmdc -i "${mmdFile}" -o "${svgFile}" --puppeteerConfigFile /tmp/puppeteer-config.json`, - { stdio: 'pipe' } - ); + await mermaid.parse(diagram); console.log(` [OK] diagram ${i + 1}`); } catch (err) { - const msg = (err.stderr || err.stdout || '').toString().split('\n')[0]; + const msg = String(err.message || err).split('\n')[0]; console.error(` [FAIL] diagram ${i + 1}: ${msg}`); console.log(`::warning file=${mdFile}::Mermaid diagram ${i + 1} failed: ${msg}`); errors++; } - }); + } } - console.log(`\nTotal diagrams checked: ${total}. Failures: ${errors}`); + console.log(`\nTotal: ${total}. Failures: ${errors}`); if (errors > 0) { console.log(`::warning::${errors} Mermaid diagram(s) failed to parse.`); process.exit(1); } - EOF + SCRIPT + node check-mermaid.mjs From 43cb3a0d17ba3bca3cd46a5a1e0be69651530cda Mon Sep 17 00:00:00 2001 From: Gronod Date: Sun, 17 May 2026 18:51:16 +0100 Subject: [PATCH 5/7] ci: trigger docs-check workflow --- docs/ARCHITECTURE.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 86b04d6..33c6936 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -1470,3 +1470,4 @@ flowchart TD style AG fill:#f8d7da ``` + From 42d01da7f7a069bdf900e0e9fd5c5c87867acf2b Mon Sep 17 00:00:00 2001 From: Gronod Date: Sun, 17 May 2026 18:58:43 +0100 Subject: [PATCH 6/7] =?UTF-8?q?ci:=20fix=20mermaid=20parse=20=E2=80=94=20u?= =?UTF-8?q?se=20jsdom=20to=20provide=20browser=20globals=20required=20by?= =?UTF-8?q?=20mermaid.core.mjs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/workflows/docs-check.yml | 76 +++++++++++++++++++-------------- 1 file changed, 44 insertions(+), 32 deletions(-) diff --git a/.gitea/workflows/docs-check.yml b/.gitea/workflows/docs-check.yml index f05352b..14f4e86 100644 --- a/.gitea/workflows/docs-check.yml +++ b/.gitea/workflows/docs-check.yml @@ -43,20 +43,29 @@ jobs: with: node-version: "22" - - name: Install mermaid - run: npm install mermaid + - name: Install mermaid and jsdom + run: npm install mermaid jsdom - name: Extract and validate Mermaid diagrams run: | - cat > check-mermaid.mjs << 'SCRIPT' - import { readFileSync, readdirSync } from 'fs'; - import { join } from 'path'; - import mermaid from './node_modules/mermaid/dist/mermaid.core.mjs'; + cat > check-mermaid.cjs << 'SCRIPT' + const { JSDOM } = require('jsdom'); + const fs = require('fs'); + const path = require('path'); + + // Provide minimal browser globals so mermaid.parse() works in Node + const dom = new JSDOM('', { url: 'http://localhost' }); + globalThis.window = dom.window; + globalThis.document = dom.window.document; + globalThis.DOMPurify = { + addHook: () => {}, removeHook: () => {}, setConfig: () => {}, + sanitize: (s) => s, isValidAttribute: () => true, + }; function findMdFiles(dir) { const out = []; - for (const e of readdirSync(dir, { withFileTypes: true })) { - const full = join(dir, e.name); + for (const e of fs.readdirSync(dir, { withFileTypes: true })) { + const full = path.join(dir, e.name); if (e.isDirectory() && e.name !== 'node_modules' && !e.name.startsWith('.')) out.push(...findMdFiles(full)); else if (e.isFile() && e.name.endsWith('.md')) @@ -65,32 +74,35 @@ jobs: return out; } - let errors = 0, total = 0; + import('./node_modules/mermaid/dist/mermaid.core.mjs').then(async (m) => { + const mermaid = m.default; + let errors = 0, total = 0; - for (const mdFile of findMdFiles('.')) { - const content = readFileSync(mdFile, 'utf8'); - const blocks = [...content.matchAll(/^```mermaid\n([\s\S]*?)^```/gm)]; - if (!blocks.length) continue; - console.log(`\nChecking ${mdFile} (${blocks.length} diagram(s))`); - for (let i = 0; i < blocks.length; i++) { - total++; - const diagram = blocks[i][1].trim(); - try { - await mermaid.parse(diagram); - console.log(` [OK] diagram ${i + 1}`); - } catch (err) { - const msg = String(err.message || err).split('\n')[0]; - console.error(` [FAIL] diagram ${i + 1}: ${msg}`); - console.log(`::warning file=${mdFile}::Mermaid diagram ${i + 1} failed: ${msg}`); - errors++; + for (const mdFile of findMdFiles('.')) { + const content = fs.readFileSync(mdFile, 'utf8'); + const blocks = [...content.matchAll(/^```mermaid\n([\s\S]*?)^```/gm)]; + if (!blocks.length) continue; + console.log(`\nChecking ${mdFile} (${blocks.length} diagram(s))`); + for (let i = 0; i < blocks.length; i++) { + total++; + const diagram = blocks[i][1].trim(); + try { + await mermaid.parse(diagram); + console.log(` [OK] diagram ${i + 1}`); + } catch (err) { + const msg = String(err.message || err).split('\n')[0]; + console.error(` [FAIL] diagram ${i + 1}: ${msg}`); + console.log(`::warning file=${mdFile}::Mermaid diagram ${i + 1} failed: ${msg}`); + errors++; + } } } - } - console.log(`\nTotal: ${total}. Failures: ${errors}`); - if (errors > 0) { - console.log(`::warning::${errors} Mermaid diagram(s) failed to parse.`); - process.exit(1); - } + console.log(`\nTotal: ${total}. Failures: ${errors}`); + if (errors > 0) { + console.log(`::warning::${errors} Mermaid diagram(s) failed to parse.`); + process.exit(1); + } + }).catch(e => { console.error('Fatal:', e.message); process.exit(1); }); SCRIPT - node check-mermaid.mjs + node check-mermaid.cjs From fce8a9ece6657decee6b0d6a2e0611bfc7aff835 Mon Sep 17 00:00:00 2001 From: Gronod Date: Sun, 17 May 2026 18:58:43 +0100 Subject: [PATCH 7/7] ci: trigger docs-check workflow --- docs/ARCHITECTURE.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 33c6936..4afe98e 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -1471,3 +1471,4 @@ flowchart TD ``` +