From 7b81f5d95922ab0bd82138026fe18ef3c4c94a63 Mon Sep 17 00:00:00 2001 From: Mubi <150862765+cicerorph@users.noreply.github.com> Date: Thu, 12 Nov 2026 10:02:37 -0300 Subject: [PATCH 1/4] add ci helps with when people that didn't add or forgot to change something properly in extensions.js in pull requests, also bcs it validates commits because yes (btw I think publish should wait for this workflow before deploying...) --- .github/workflows/ci.yml | 46 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..ecfe02a48 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,46 @@ +name: ci + +on: + push: + pull_request: + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + +jobs: + validate-extensions: + name: validate extensions + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: install node + uses: actions/setup-node@v4 + with: + node-version: 22.x + + - name: validate extensions.js and extension code + run: node .github/scripts/validate_extensions.cjs + + build: + name: build site + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: install node + uses: actions/setup-node@v4 + with: + node-version: 22.x + cache: npm + + - name: install dependencies + run: npm ci + + - name: build + run: npm run build From 19da491454ed9938f43b4648981806166c9beeb5 Mon Sep 17 00:00:00 2001 From: Mubi <150862765+cicerorph@users.noreply.github.com> Date: Thu, 12 Nov 2026 10:51:24 -0300 Subject: [PATCH 2/4] script --- .github/scripts/validate_extensions.cjs | 104 ++++++++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 .github/scripts/validate_extensions.cjs diff --git a/.github/scripts/validate_extensions.cjs b/.github/scripts/validate_extensions.cjs new file mode 100644 index 000000000..f75de3aab --- /dev/null +++ b/.github/scripts/validate_extensions.cjs @@ -0,0 +1,104 @@ +const fs = require('fs'); +const os = require('os'); +const path = require('path'); +const vm = require('vm'); +const { pathToFileURL } = require('url'); + +const FILE = 'src/lib/extensions.js'; +const CODE_DIR = 'static/extensions'; +const BANNER_DIR = 'static/images'; + +let errors = 0; + +const esc = (s) => String(s).replace(/%/g, '%25').replace(/\r/g, '%0D').replace(/\n/g, '%0A'); +function fail(msg, file = FILE, line) { + errors++; + console.log(`::error file=${file}${line ? `,line=${line}` : ''}::${esc(msg)}`); +} +const warn = (msg, file = FILE) => console.log(`::warning file=${file}::${esc(msg)}`); + +function walk(dir) { + if (!fs.existsSync(dir)) return []; + return fs.readdirSync(dir, { withFileTypes: true }).flatMap((d) => { + const p = path.join(dir, d.name); + return d.isDirectory() ? walk(p) : [p]; + }); +} + +function safeResolve(baseDir, rel) { + const base = path.resolve(baseDir); + const full = path.resolve(base, rel); + return full.startsWith(base + path.sep) ? full : null; +} + +async function loadExtensions() { + const tmp = path.join(os.tmpdir(), `extensions-validate-${Date.now()}.mjs`); + fs.writeFileSync(tmp, fs.readFileSync(FILE, 'utf8')); + const mod = await import(pathToFileURL(tmp).href); + return mod.default; +} + +(async () => { + let list; + try { + list = await loadExtensions(); + } catch (err) { + fail(`Could not load the file: ${err.message}`); + process.exit(1); + } + + const seen = new Set(); + const referenced = new Set(); + + list.forEach((ext, i) => { + const label = ext && ext.code ? ext.code : `#${i}`; + + if (!ext || typeof ext !== 'object') return fail(`Entry ${label} is not an object.`); + + if (typeof ext.code !== 'string' || !ext.code.trim()) { + return fail(`Entry ${label} has no valid "code".`); + } + if (seen.has(ext.code)) fail(`Duplicate "code": ${ext.code}`); + seen.add(ext.code); + + if (ext.tags !== undefined && !Array.isArray(ext.tags)) { + fail(`${label}: "tags" must be an array.`); + } + + const codePath = safeResolve(CODE_DIR, ext.code); + if (!codePath) return fail(`${label}: invalid path.`); + if (!fs.existsSync(codePath)) { + return fail(`${label}: file not found at ${CODE_DIR}/${ext.code}`); + } + referenced.add(codePath); + + const rel = path.relative(process.cwd(), codePath); + const src = fs.readFileSync(codePath, 'utf8'); + if (!src.trim()) { + fail(`${label}: file is empty.`, rel); + } else { + try { + new vm.Script(src, { filename: rel }); + } catch (err) { + const m = /:(\d+)$/.exec(String(err.stack).split('\n')[0]); + fail(`${label}: syntax error: ${err.message}`, rel, m ? m[1] : undefined); + } + } + + if (typeof ext.banner === 'string') { + const bannerPath = safeResolve(BANNER_DIR, ext.banner); + if (!bannerPath || !fs.existsSync(bannerPath)) { + fail(`${label}: banner not found at ${BANNER_DIR}/${ext.banner}`); + } + } + }); + + for (const f of walk(CODE_DIR)) { + if (f.endsWith('.js') && !referenced.has(path.resolve(f))) { + warn(`Extension not registered in ${FILE}`, f); + } + } + + console.log(`${list.length} extensions checked, ${errors} error(s).`); + if (errors) process.exit(1); +})(); \ No newline at end of file From 79eb07873d8bae2f70373fa8b1b09406216be0c0 Mon Sep 17 00:00:00 2001 From: Mubi <150862765+cicerorph@users.noreply.github.com> Date: Thu, 12 Nov 2026 19:34:27 -0300 Subject: [PATCH 3/4] make so we can fetch the commit history --- .github/workflows/ci.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ecfe02a48..03bccc644 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,6 +18,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + with: + fetch-depth: 0 - name: install node uses: actions/setup-node@v4 @@ -25,6 +27,8 @@ jobs: node-version: 22.x - name: validate extensions.js and extension code + env: + BASE_SHA: ${{ github.event.pull_request.base.sha }} run: node .github/scripts/validate_extensions.cjs build: From 7eb6389388ca2e5dc4f1c7a73503a8dcf22d9351 Mon Sep 17 00:00:00 2001 From: Mubi <150862765+cicerorph@users.noreply.github.com> Date: Thu, 12 Nov 2026 19:49:16 -0300 Subject: [PATCH 4/4] make it check the changed contents --- .github/scripts/validate_extensions.cjs | 30 +++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/.github/scripts/validate_extensions.cjs b/.github/scripts/validate_extensions.cjs index f75de3aab..f19bf3f06 100644 --- a/.github/scripts/validate_extensions.cjs +++ b/.github/scripts/validate_extensions.cjs @@ -3,6 +3,7 @@ const os = require('os'); const path = require('path'); const vm = require('vm'); const { pathToFileURL } = require('url'); +const { execFileSync } = require('child_process'); const FILE = 'src/lib/extensions.js'; const CODE_DIR = 'static/extensions'; @@ -31,6 +32,24 @@ function safeResolve(baseDir, rel) { return full.startsWith(base + path.sep) ? full : null; } +function changedFiles() { + const base = + process.env.BASE_SHA || + (process.env.GITHUB_BASE_REF ? `origin/${process.env.GITHUB_BASE_REF}` : null); + if (!base) return null; + try { + const out = execFileSync( + 'git', + ['diff', '--name-only', '--diff-filter=AMR', `${base}...HEAD`, '--', CODE_DIR], + { encoding: 'utf8' } + ); + return out.split('\n').filter(Boolean).map((f) => path.resolve(f)); + } catch (err) { + console.log(`Could not diff against ${base}: ${err.message}`); + return null; + } +} + async function loadExtensions() { const tmp = path.join(os.tmpdir(), `extensions-validate-${Date.now()}.mjs`); fs.writeFileSync(tmp, fs.readFileSync(FILE, 'utf8')); @@ -93,12 +112,15 @@ async function loadExtensions() { } }); - for (const f of walk(CODE_DIR)) { - if (f.endsWith('.js') && !referenced.has(path.resolve(f))) { - warn(`Extension not registered in ${FILE}`, f); + const changed = changedFiles(); + if (changed) { + for (const f of changed) { + if (f.endsWith('.js') && !referenced.has(f)) { + warn(`Extension not registered in ${FILE}`, path.relative(process.cwd(), f)); + } } } console.log(`${list.length} extensions checked, ${errors} error(s).`); if (errors) process.exit(1); -})(); \ No newline at end of file +})();