From 47ae562cc92571ed119a3c1b5eeb98f79052b813 Mon Sep 17 00:00:00 2001 From: Radek Sienkiewicz Date: Fri, 27 Mar 2026 18:31:19 +0100 Subject: [PATCH] Docs: unify link audit entrypoint (#55912) Merged via squash. Prepared head SHA: 6b1ccb9f1fb1add05df90d7db233667a49edf0b5 Co-authored-by: velvet-shark <126378+velvet-shark@users.noreply.github.com> Co-authored-by: velvet-shark <126378+velvet-shark@users.noreply.github.com> Reviewed-by: @velvet-shark --- CHANGELOG.md | 1 + docs/help/testing.md | 3 +- package.json | 1 + scripts/docs-link-audit.mjs | 44 ++++++++++++++---- src/scripts/docs-link-audit.test.ts | 70 ++++++++++++++++++++++++++++- 5 files changed, 109 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 000f55bd114..cfd555ee12f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ Docs: https://docs.openclaw.ai - Plugins/startup: auto-load bundled provider and CLI-backend plugins from explicit config refs, so bundled Claude CLI, Codex CLI, and Gemini CLI message-provider setups no longer need manual `plugins.allow` entries. - Config/TTS: auto-migrate legacy speech config on normal reads and secret resolution, keep legacy diagnostics for Doctor, and remove regular-mode runtime fallback for old bundled `tts.` API-key shapes. - OpenAI/apply_patch: enable `apply_patch` by default for OpenAI and OpenAI Codex models, and align its sandbox policy access with `write` permissions. +- Docs: add `pnpm docs:check-links:anchors` for Mintlify anchor validation while keeping `scripts/docs-link-audit.mjs` as the stable link-audit entrypoint. (#55912) Thanks @velvet-shark. ### Fixes diff --git a/docs/help/testing.md b/docs/help/testing.md index 3454f7b9578..0f0f071bf42 100644 --- a/docs/help/testing.md +++ b/docs/help/testing.md @@ -506,7 +506,8 @@ Useful env vars: ## Docs sanity -Run docs checks after doc edits: `pnpm docs:list`. +Run docs checks after doc edits: `pnpm check:docs`. +Run full Mintlify anchor validation when you need in-page heading checks too: `pnpm docs:check-links:anchors`. ## Offline regression (CI-safe) diff --git a/package.json b/package.json index 5b48331a519..f6c8a0c3515 100644 --- a/package.json +++ b/package.json @@ -717,6 +717,7 @@ "docs:bin": "node scripts/build-docs-list.mjs", "docs:check-i18n-glossary": "node scripts/check-docs-i18n-glossary.mjs", "docs:check-links": "node scripts/docs-link-audit.mjs", + "docs:check-links:anchors": "node scripts/docs-link-audit.mjs --anchors", "docs:dev": "cd docs && mint dev", "docs:list": "node scripts/docs-list.js", "docs:spellcheck": "bash scripts/docs-spellcheck.sh", diff --git a/scripts/docs-link-audit.mjs b/scripts/docs-link-audit.mjs index e60bff8f3ee..ad0e40f3264 100644 --- a/scripts/docs-link-audit.mjs +++ b/scripts/docs-link-audit.mjs @@ -1,5 +1,6 @@ #!/usr/bin/env node +import { spawnSync } from "node:child_process"; import fs from "node:fs"; import path from "node:path"; import { pathToFileURL } from "node:url"; @@ -288,12 +289,32 @@ export function auditDocsLinks() { return { checked, broken }; } -function isCliEntry() { - const cliArg = process.argv[1]; - return cliArg ? import.meta.url === pathToFileURL(cliArg).href : false; -} +/** + * @param {{ + * args?: string[]; + * spawnSyncImpl?: typeof spawnSync; + * }} [options] + */ +export function runDocsLinkAuditCli(options = {}) { + const args = options.args ?? process.argv.slice(2); + if (args.includes("--anchors")) { + const spawnSyncImpl = options.spawnSyncImpl ?? spawnSync; + const result = spawnSyncImpl("mint", ["broken-links", "--check-anchors"], { + cwd: DOCS_DIR, + stdio: "inherit", + }); + + if (result.error?.code === "ENOENT") { + const fallback = spawnSyncImpl("pnpm", ["dlx", "mint", "broken-links", "--check-anchors"], { + cwd: DOCS_DIR, + stdio: "inherit", + }); + return fallback.status ?? 1; + } + + return result.status ?? 1; + } -if (isCliEntry()) { const { checked, broken } = auditDocsLinks(); console.log(`checked_internal_links=${checked}`); console.log(`broken_links=${broken.length}`); @@ -302,7 +323,14 @@ if (isCliEntry()) { console.log(`${item.file}:${item.line} :: ${item.link} :: ${item.reason}`); } - if (broken.length > 0) { - process.exit(1); - } + return broken.length > 0 ? 1 : 0; +} + +function isCliEntry() { + const cliArg = process.argv[1]; + return cliArg ? import.meta.url === pathToFileURL(cliArg).href : false; +} + +if (isCliEntry()) { + process.exit(runDocsLinkAuditCli()); } diff --git a/src/scripts/docs-link-audit.test.ts b/src/scripts/docs-link-audit.test.ts index 34b9958d3f4..01fb0d362f6 100644 --- a/src/scripts/docs-link-audit.test.ts +++ b/src/scripts/docs-link-audit.test.ts @@ -1,12 +1,21 @@ +import path from "node:path"; import { describe, expect, it } from "vitest"; -const { normalizeRoute, resolveRoute } = +const { normalizeRoute, resolveRoute, runDocsLinkAuditCli } = (await import("../../scripts/docs-link-audit.mjs")) as unknown as { normalizeRoute: (route: string) => string; resolveRoute: ( route: string, options?: { redirects?: Map; routes?: Set }, ) => { ok: boolean; terminal: string; loop?: boolean }; + runDocsLinkAuditCli: (options?: { + args?: string[]; + spawnSyncImpl?: ( + command: string, + args: string[], + options: { cwd: string; stdio: string }, + ) => { status: number | null; error?: { code?: string } }; + }) => number; }; describe("docs-link-audit", () => { @@ -28,4 +37,63 @@ describe("docs-link-audit", () => { terminal: "/plugins/building-plugins", }); }); + + it("prefers a local mint binary for anchor validation", () => { + let invocation: + | { + command: string; + args: string[]; + options: { cwd: string; stdio: string }; + } + | undefined; + + const exitCode = runDocsLinkAuditCli({ + args: ["--anchors"], + spawnSyncImpl(command, args, options) { + invocation = { command, args, options }; + return { status: 0 }; + }, + }); + + expect(exitCode).toBe(0); + expect(invocation).toBeDefined(); + expect(invocation?.command).toBe("mint"); + expect(invocation?.args).toEqual(["broken-links", "--check-anchors"]); + expect(invocation?.options.stdio).toBe("inherit"); + expect(path.basename(invocation?.options.cwd ?? "")).toBe("docs"); + }); + + it("falls back to pnpm dlx when mint is not on PATH", () => { + const invocations: Array<{ + command: string; + args: string[]; + options: { cwd: string; stdio: string }; + }> = []; + + const exitCode = runDocsLinkAuditCli({ + args: ["--anchors"], + spawnSyncImpl(command, args, options) { + invocations.push({ command, args, options }); + if (command === "mint") { + return { status: null, error: { code: "ENOENT" } }; + } + return { status: 0 }; + }, + }); + + expect(exitCode).toBe(0); + expect(invocations).toHaveLength(2); + expect(invocations[0]).toMatchObject({ + command: "mint", + args: ["broken-links", "--check-anchors"], + options: { stdio: "inherit" }, + }); + expect(invocations[1]).toMatchObject({ + command: "pnpm", + args: ["dlx", "mint", "broken-links", "--check-anchors"], + options: { stdio: "inherit" }, + }); + expect(path.basename(invocations[0]?.options.cwd ?? "")).toBe("docs"); + expect(path.basename(invocations[1]?.options.cwd ?? "")).toBe("docs"); + }); });