Docs: unify link audit entrypoint (#55912)

Merged via squash.

Prepared head SHA: 6b1ccb9f1f
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
This commit is contained in:
Radek Sienkiewicz 2026-03-27 18:31:19 +01:00 committed by GitHub
parent d35f37a58c
commit 47ae562cc9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 109 additions and 10 deletions

View File

@ -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.<provider>` 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

View File

@ -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)

View File

@ -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",

View File

@ -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());
}

View File

@ -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<string, string>; routes?: Set<string> },
) => { 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");
});
});