From 283df32afef27bd70707666206b6f2cdaa3c0b60 Mon Sep 17 00:00:00 2001 From: Andras Schmelczer Date: Fri, 26 Jun 2026 00:01:37 +0200 Subject: [PATCH] No github --- .forgejo/workflows/deploy.yml | 1 + scripts/check-no-github-links.mjs | 100 ++++++++++++++++++++++++++++++ 2 files changed, 101 insertions(+) create mode 100644 scripts/check-no-github-links.mjs diff --git a/.forgejo/workflows/deploy.yml b/.forgejo/workflows/deploy.yml index a2796f4..7a93e49 100644 --- a/.forgejo/workflows/deploy.yml +++ b/.forgejo/workflows/deploy.yml @@ -35,6 +35,7 @@ jobs: npm run typecheck npm run qa:no-em-dashes npm run qa:spelling + npm run qa:no-github-links - name: Build run: npm run build diff --git a/scripts/check-no-github-links.mjs b/scripts/check-no-github-links.mjs new file mode 100644 index 0000000..d6a25c3 --- /dev/null +++ b/scripts/check-no-github-links.mjs @@ -0,0 +1,100 @@ +// cspell:ignore githubusercontent streetsidesoftware forgejo +import { readFile, stat } from 'node:fs/promises'; +import path from 'node:path'; +import { walk } from './lib/walk.mjs'; + +const root = process.cwd(); + +// GitHub is no longer linked from anywhere on the site: project source links, +// the social links, and the repo metadata all point at the self-hosted git +// host instead. Matching the host substrings (rather than the bare word +// "github") keeps the Shiki `github-light`/`github-dark` themes and the +// Forgejo Actions `github.*` context variables from tripping the check. +const bannedHosts = [/github\.com/i, /github\.io/i, /githubusercontent\.com/i]; + +// Upstream tooling URLs that genuinely live on GitHub and have no self-hosted +// equivalent (e.g. the cspell config JSON Schema). A flagged URL is permitted +// only when it exactly equals one of these. +const permitted = new Set([ + 'https://raw.githubusercontent.com/streetsidesoftware/cspell/main/cspell.schema.json', +]); + +const textExtensions = new Set([ + '.astro', + '.css', + '.html', + '.js', + '.json', + '.md', + '.mjs', + '.ts', + '.txt', + '.vtt', + '.webmanifest', + '.xml', +]); + +const roots = [ + 'src', + 'public', + 'scripts', + 'README.md', + 'package.json', + 'astro.config.mjs', + 'cspell.json', +].map((entry) => path.resolve(root, entry)); + +async function exists(filePath) { + try { + await stat(filePath); + return true; + } catch { + return false; + } +} + +// Grows a match outward to the whole whitespace/quote-delimited URL token so it +// can be compared against the permitted set. +function urlToken(line, index, length) { + const boundary = /[\s"'`<>()[\]{}]/; + let start = index; + while (start > 0 && !boundary.test(line[start - 1])) start -= 1; + let end = index + length; + while (end < line.length && !boundary.test(line[end])) end += 1; + return line.slice(start, end); +} + +const files = []; + +for (const entry of roots) { + if (!(await exists(entry))) continue; + files.push(...(await walk(entry))); +} + +const textFiles = files.filter((file) => textExtensions.has(path.extname(file))); + +const failures = []; + +for (const file of textFiles) { + const lines = (await readFile(file, 'utf8')).split('\n'); + + lines.forEach((line, lineIndex) => { + for (const pattern of bannedHosts) { + for (const match of line.matchAll(new RegExp(pattern, 'gi'))) { + const token = urlToken(line, match.index, match[0].length); + if (permitted.has(token)) continue; + const location = `${path.relative(root, file)}:${lineIndex + 1}:${match.index + 1}`; + failures.push(location); + } + } + }); +} + +if (failures.length > 0) { + console.error( + `GitHub links are not allowed; link the self-hosted git host instead:\n${failures.join('\n')}` + ); + process.exit(1); +} + +console.log('No GitHub links found.');