From af07f790719616d9f50956b18897c293136baa5b Mon Sep 17 00:00:00 2001 From: NetVar1337 <89574102+NetVar1337@users.noreply.github.com> Date: Mon, 26 Oct 2026 09:10:27 +0100 Subject: [PATCH 1/2] ci: add local docs checks (mintlify validate, broken-links, EN/KO parity) - scripts/check-parity.mjs: compares en/ vs ko/ page files, docs.json nav per language, and nav <-> files; exits 1 listing every mismatch - scripts/ci.sh: runs validate, broken-links and parity; logs to /workspace/out/local-ci-/ (local CI is the primary path) - README: preview + running checks --- .gitignore | 1 + README.md | 34 +++++++++++++++++ scripts/check-parity.mjs | 82 ++++++++++++++++++++++++++++++++++++++++ scripts/ci.sh | 51 +++++++++++++++++++++++++ 4 files changed, 168 insertions(+) create mode 100644 .gitignore create mode 100755 scripts/check-parity.mjs create mode 100755 scripts/ci.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0172655 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +ci-logs/ diff --git a/README.md b/README.md index e69de29..186e80c 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,34 @@ +# Decepticon Docs + +Source for the Decepticon documentation by Bitter Security, built with [Mintlify](https://mintlify.com). +Site config and navigation live in `docs.json`; pages are MDX under `en/` and `ko/` (kept 1:1). + +## Preview locally + +```bash +npx mintlify@latest dev +``` + +## Running checks + +CI runs **locally** (we don't use paid GitHub Actions minutes), so run this before opening a PR: + +```bash +scripts/ci.sh +``` + +It runs, in order: + +| Step | Command | Fails when | +|---|---|---| +| Build validation | `npx mintlify validate` | `docs.json` / MDX doesn't build | +| Link check | `npx mintlify broken-links` | any internal link points at a missing page | +| EN/KO parity | `node scripts/check-parity.mjs` | a page exists in one language but not the other, a `docs.json` nav entry has no twin in the other language, a nav entry has no file, or a page file isn't in the nav | + +Logs go to `/workspace/out/local-ci-/` (`validate.log`, `broken-links.log`, `parity.log`, `summary.txt`); +override with `CI_OUT_ROOT=/some/dir`. The mintlify CLI version is pinned in the script; override with `MINTLIFY_VERSION=latest`. +Requires Node 20+. + +When you add a page, add both `en/.mdx` and `ko/.mdx` and list both in `docs.json`. + +`.github/workflows/docs-ci.yml` (optional) runs the same script on pull requests if Actions is ever enabled. diff --git a/scripts/check-parity.mjs b/scripts/check-parity.mjs new file mode 100755 index 0000000..32e2672 --- /dev/null +++ b/scripts/check-parity.mjs @@ -0,0 +1,82 @@ +#!/usr/bin/env node +// EN <-> KO parity check for the Mintlify docs. +// +// Checks, relative to the repo root (or the dir given as argv[2]): +// 1. Page files: every en/**/*.mdx has a ko/**/*.mdx twin at the same relative path, and vice versa. +// 2. docs.json nav: every page listed under the "en" language has a matching "ko" entry, and vice versa. +// 3. Nav <-> files: every nav entry resolves to an existing .mdx/.md file, and every page file is in the nav. +// +// Exits 1 and lists every mismatch if anything is out of sync; exits 0 otherwise. +import { readFileSync, readdirSync, statSync, existsSync } from "node:fs"; +import { join, relative, sep } from "node:path"; + +const root = process.argv[2] ?? process.cwd(); +const LANGS = ["en", "ko"]; +const PAGE_EXT = /\.(mdx|md)$/; + +function walk(dir) { + const out = []; + if (!existsSync(dir)) return out; + for (const name of readdirSync(dir)) { + const p = join(dir, name); + if (statSync(p).isDirectory()) out.push(...walk(p)); + else if (PAGE_EXT.test(name)) out.push(p); + } + return out; +} + +// "en/foo/bar.mdx" -> "foo/bar" +const filePages = {}; +for (const lang of LANGS) { + filePages[lang] = new Set( + walk(join(root, lang)).map((p) => + relative(join(root, lang), p).split(sep).join("/").replace(PAGE_EXT, ""), + ), + ); +} + +// Collect page slugs from the docs.json navigation for each language. +function collectPages(node, acc) { + if (typeof node === "string") acc.push(node); + else if (Array.isArray(node)) node.forEach((n) => collectPages(n, acc)); + else if (node && typeof node === "object") { + for (const key of ["pages", "groups", "tabs", "anchors", "dropdowns", "versions", "menu"]) { + if (node[key]) collectPages(node[key], acc); + } + } + return acc; +} + +const docs = JSON.parse(readFileSync(join(root, "docs.json"), "utf8")); +const navLangs = docs?.navigation?.languages ?? []; +const navPages = {}; +for (const lang of LANGS) { + const entry = navLangs.find((l) => l.language === lang); + navPages[lang] = new Set( + entry ? collectPages(entry, []).map((p) => p.replace(new RegExp(`^/?${lang}/`), "")) : [], + ); + if (!entry) navPages[lang].missingLanguage = true; +} + +const problems = []; +const diff = (a, b) => [...a].filter((x) => !b.has(x)).sort(); + +for (const lang of LANGS) if (navPages[lang].missingLanguage) problems.push(`docs.json: no navigation.languages entry for "${lang}"`); + +const [A, B] = LANGS; +for (const [x, y] of [[A, B], [B, A]]) { + for (const p of diff(filePages[x], filePages[y])) problems.push(`file: ${x}/${p} exists but ${y}/${p} is missing`); + for (const p of diff(navPages[x], navPages[y])) problems.push(`nav: ${x}/${p} is in docs.json but ${y}/${p} is not`); +} +for (const lang of LANGS) { + for (const p of diff(navPages[lang], filePages[lang])) problems.push(`nav->file: ${lang}/${p} is in docs.json but has no .mdx/.md file`); + for (const p of diff(filePages[lang], navPages[lang])) problems.push(`file->nav: ${lang}/${p} exists but is not in docs.json navigation`); +} + +const counts = LANGS.map((l) => `${l}: ${filePages[l].size} files / ${navPages[l].size} nav`).join(", "); +if (problems.length) { + console.error(`parity: FAILED (${counts})`); + for (const p of problems) console.error(` - ${p}`); + process.exit(1); +} +console.log(`parity: OK (${counts})`); diff --git a/scripts/ci.sh b/scripts/ci.sh new file mode 100755 index 0000000..dab0417 --- /dev/null +++ b/scripts/ci.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash +# Local docs CI: Mintlify build validation, broken-link check, EN<->KO parity. +# +# This is the primary CI path (GitHub Actions is not used for billing reasons; +# the workflow in .github/workflows/docs-ci.yml is optional and just calls this). +# +# Usage: scripts/ci.sh +# Env: +# MINTLIFY_VERSION mintlify CLI version for npx (default: pinned below; "latest" also works) +# CI_OUT_ROOT where log dirs go (default: /workspace/out locally, ./ci-logs under $CI) +# +# Logs: $CI_OUT_ROOT/local-ci-/{validate,broken-links,parity}.log + summary.txt +set -uo pipefail + +cd "$(dirname "$0")/.." + +MINTLIFY_VERSION="${MINTLIFY_VERSION:-4.2.1126}" +SHA="$(git rev-parse --short HEAD 2>/dev/null || echo nogit)" +if ! git diff --quiet HEAD 2>/dev/null; then SHA="${SHA}-dirty"; fi +if [[ -n "${CI:-}" ]]; then DEFAULT_OUT_ROOT="$PWD/ci-logs"; else DEFAULT_OUT_ROOT="/workspace/out"; fi +OUT_DIR="${CI_OUT_ROOT:-$DEFAULT_OUT_ROOT}/local-ci-${SHA}" +mkdir -p "$OUT_DIR" + +MINT=(npx --yes "mintlify@${MINTLIFY_VERSION}") +declare -a RESULTS=() +FAILED=0 + +run_step() { + local name="$1"; shift + local log="$OUT_DIR/${name}.log" + echo "==> ${name}: $*" + if "$@" >"$log" 2>&1; then + RESULTS+=("PASS ${name} (${log})") + else + RESULTS+=("FAIL ${name} (${log})") + FAILED=1 + tail -n 40 "$log" | sed 's/^/ /' + fi +} + +run_step validate "${MINT[@]}" validate +run_step broken-links "${MINT[@]}" broken-links +run_step parity node scripts/check-parity.mjs + +{ + echo "docs local CI @ ${SHA} ($(date '+%Y-%m-%d %H:%M:%S %Z'))" + echo "mintlify@${MINTLIFY_VERSION}, node $(node --version)" + printf '%s\n' "${RESULTS[@]}" +} | tee "$OUT_DIR/summary.txt" + +exit "$FAILED" From 34aeb0dc898d40dd9dba3013d08488e01abe9dba Mon Sep 17 00:00:00 2001 From: NetVar1337 <89574102+NetVar1337@users.noreply.github.com> Date: Mon, 26 Oct 2026 09:10:27 +0100 Subject: [PATCH 2/2] ci: add optional GitHub Actions workflow calling scripts/ci.sh Kept as a separate commit so it can be dropped if Actions stays unpaid. --- .github/workflows/docs-ci.yml | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 .github/workflows/docs-ci.yml diff --git a/.github/workflows/docs-ci.yml b/.github/workflows/docs-ci.yml new file mode 100644 index 0000000..985ba8d --- /dev/null +++ b/.github/workflows/docs-ci.yml @@ -0,0 +1,27 @@ +# Optional: the primary CI path is running scripts/ci.sh locally (see README). +# This workflow only calls the same script; drop this commit if Actions stays disabled. +name: docs-ci + +on: + pull_request: + +permissions: + contents: read + +jobs: + docs-ci: + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: "20" + - name: Run docs checks + run: scripts/ci.sh + - name: Upload logs + if: always() + uses: actions/upload-artifact@v4 + with: + name: docs-ci-logs + path: ci-logs/