mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:50:43 +00:00
ci: validate docs mdx before publish
This commit is contained in:
2
.github/codex/prompts/docs-agent.md
vendored
2
.github/codex/prompts/docs-agent.md
vendored
@@ -28,4 +28,6 @@ Required workflow:
|
||||
4. Run `pnpm check:docs` if dependencies are available.
|
||||
5. Leave the worktree clean if no docs need changes.
|
||||
|
||||
If `pnpm docs:check-mdx` or `pnpm check:docs` reports MDX parse errors, fix only the syntax needed for the listed existing docs files. Preserve prose meaning, frontmatter, code fences, and links; do not broadly rewrite translated or source content while repairing parser failures.
|
||||
|
||||
When uncertain, prefer no edit and explain the uncertainty in the final message.
|
||||
|
||||
25
.github/codex/prompts/docs-mdx-repair.md
vendored
Normal file
25
.github/codex/prompts/docs-mdx-repair.md
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
# OpenClaw Docs MDX Repair Agent
|
||||
|
||||
You are repairing generated OpenClaw documentation after a fast MDX validation failure.
|
||||
|
||||
Goal: fix only the MDX syntax errors reported by the checker.
|
||||
|
||||
Hard limits:
|
||||
|
||||
- Edit only existing Markdown/MDX files under the locale path named by `LOCALE`.
|
||||
- Do not edit source English docs unless `LOCALE=en`.
|
||||
- Do not edit code, workflows, package metadata, generated sync metadata, translation memory, or assets.
|
||||
- Do not add, delete, or rename files.
|
||||
- Preserve the meaning of translated prose.
|
||||
- Preserve frontmatter, `x-i18n.source_hash`, links, code fences, JSX component names, and existing page structure.
|
||||
- Avoid broad formatting or retranslation.
|
||||
|
||||
Required workflow:
|
||||
|
||||
1. Read `.openclaw-sync/mdx/${LOCALE}.json` when it exists.
|
||||
2. Inspect only the listed files and nearby lines.
|
||||
3. Fix the minimal syntax issue, such as broken JSX attribute quoting, mismatched component closing tags, raw `<` text, raw HTML comments, or accidental top-level `import`/`export` text.
|
||||
4. Run `node source/scripts/check-docs-mdx.mjs "docs/${LOCALE}" --json-out ".openclaw-sync/mdx/${LOCALE}.json"`.
|
||||
5. Leave no changes outside `docs/${LOCALE}`.
|
||||
|
||||
When uncertain, prefer the smallest escaping fix: backticks for literal words, `<` for literal `<`, double quotes around JSX attribute values, and balanced component tags.
|
||||
6
.github/workflows/docs-sync-publish.yml
vendored
6
.github/workflows/docs-sync-publish.yml
vendored
@@ -43,6 +43,12 @@ jobs:
|
||||
--source-repo "$GITHUB_REPOSITORY" \
|
||||
--source-sha "$GITHUB_SHA"
|
||||
|
||||
- name: Install docs MDX checker dependency
|
||||
run: npm install --no-save --package-lock=false @mdx-js/mdx@3.1.1
|
||||
|
||||
- name: Check publish docs MDX
|
||||
run: node "$GITHUB_WORKSPACE/publish/.openclaw-sync/check-docs-mdx.mjs" "$GITHUB_WORKSPACE/publish/docs"
|
||||
|
||||
- name: Commit publish repo sync
|
||||
working-directory: publish
|
||||
run: |
|
||||
|
||||
@@ -1263,7 +1263,7 @@
|
||||
"check:base-config-schema": "node --import tsx scripts/generate-base-config-schema.ts --check",
|
||||
"check:bundled-channel-config-metadata": "node --import tsx scripts/generate-bundled-channel-config-metadata.ts --check",
|
||||
"check:changed": "node scripts/check-changed.mjs",
|
||||
"check:docs": "pnpm format:docs:check && pnpm lint:docs && pnpm docs:check-i18n-glossary && pnpm docs:check-links",
|
||||
"check:docs": "pnpm format:docs:check && pnpm lint:docs && pnpm docs:check-mdx && pnpm docs:check-i18n-glossary && pnpm docs:check-links",
|
||||
"check:host-env-policy:swift": "node scripts/generate-host-env-security-policy-swift.mjs --check",
|
||||
"check:import-cycles": "node --import tsx scripts/check-import-cycles.ts",
|
||||
"check:loc": "node --import tsx scripts/check-ts-max-loc.ts --max 500",
|
||||
@@ -1298,6 +1298,7 @@
|
||||
"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:check-mdx": "node scripts/check-docs-mdx.mjs docs README.md",
|
||||
"docs:dev": "cd docs && mint dev",
|
||||
"docs:list": "node scripts/docs-list.js",
|
||||
"docs:spellcheck": "bash scripts/docs-spellcheck.sh",
|
||||
@@ -1589,6 +1590,7 @@
|
||||
"@grammyjs/types": "^3.26.0",
|
||||
"@lit-labs/signals": "^0.2.0",
|
||||
"@lit/context": "^1.1.6",
|
||||
"@mdx-js/mdx": "^3.1.1",
|
||||
"@types/express": "^5.0.6",
|
||||
"@types/markdown-it": "^14.1.2",
|
||||
"@types/node": "25.6.0",
|
||||
|
||||
775
pnpm-lock.yaml
generated
775
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
270
scripts/check-docs-mdx.mjs
Normal file
270
scripts/check-docs-mdx.mjs
Normal file
@@ -0,0 +1,270 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { compile } from "@mdx-js/mdx";
|
||||
|
||||
const MINTLIFY_LANGUAGE_CODES = new Set([
|
||||
"en",
|
||||
"cn",
|
||||
"zh",
|
||||
"zh-Hans",
|
||||
"zh-Hant",
|
||||
"es",
|
||||
"fr",
|
||||
"fr-CA",
|
||||
"fr-ca",
|
||||
"ja",
|
||||
"jp",
|
||||
"ja-jp",
|
||||
"pt",
|
||||
"pt-BR",
|
||||
"de",
|
||||
"ko",
|
||||
"it",
|
||||
"ru",
|
||||
"ro",
|
||||
"cs",
|
||||
"id",
|
||||
"ar",
|
||||
"tr",
|
||||
"hi",
|
||||
"sv",
|
||||
"no",
|
||||
"lv",
|
||||
"nl",
|
||||
"uk",
|
||||
"vi",
|
||||
"pl",
|
||||
"uz",
|
||||
"he",
|
||||
"ca",
|
||||
"fi",
|
||||
"hu",
|
||||
]);
|
||||
|
||||
function parseArgs(argv) {
|
||||
const roots = [];
|
||||
let jsonOut = "";
|
||||
let maxErrors = 50;
|
||||
|
||||
for (let index = 0; index < argv.length; index += 1) {
|
||||
const part = argv[index];
|
||||
if (part === "--json-out") {
|
||||
jsonOut = argv[index + 1] ?? "";
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
if (part === "--max-errors") {
|
||||
maxErrors = Number.parseInt(argv[index + 1] ?? "", 10);
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
if (part.startsWith("--")) {
|
||||
throw new Error(`unknown arg: ${part}`);
|
||||
}
|
||||
roots.push(part);
|
||||
}
|
||||
|
||||
return {
|
||||
roots: roots.length ? roots : ["docs"],
|
||||
jsonOut,
|
||||
maxErrors: Number.isFinite(maxErrors) && maxErrors > 0 ? maxErrors : 50,
|
||||
};
|
||||
}
|
||||
|
||||
function walkMarkdownFiles(entryPath, out = []) {
|
||||
const stat = fs.statSync(entryPath);
|
||||
if (stat.isFile()) {
|
||||
if (/\.mdx?$/i.test(entryPath)) {
|
||||
out.push(path.resolve(entryPath));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
for (const entry of fs.readdirSync(entryPath, { withFileTypes: true })) {
|
||||
if (entry.name === "node_modules" || entry.name === ".git") {
|
||||
continue;
|
||||
}
|
||||
walkMarkdownFiles(path.join(entryPath, entry.name), out);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function stripFrontmatter(raw) {
|
||||
if (!raw.startsWith("---\n") && !raw.startsWith("---\r\n")) {
|
||||
return raw;
|
||||
}
|
||||
|
||||
const lines = raw.split(/\r?\n/u);
|
||||
for (let index = 1; index < lines.length; index += 1) {
|
||||
if (lines[index] === "---" || lines[index] === "...") {
|
||||
return lines.slice(index + 1).join("\n");
|
||||
}
|
||||
}
|
||||
return raw;
|
||||
}
|
||||
|
||||
function formatMdxError(filePath, error) {
|
||||
const place = error?.place ?? error?.position;
|
||||
const start = place?.start ?? place;
|
||||
const line = typeof start?.line === "number" ? start.line : undefined;
|
||||
const column = typeof start?.column === "number" ? start.column : undefined;
|
||||
return {
|
||||
type: "mdx",
|
||||
file: filePath,
|
||||
line,
|
||||
column,
|
||||
message: String(error?.reason ?? error?.message ?? error).split("\n")[0],
|
||||
};
|
||||
}
|
||||
|
||||
async function checkMdxFile(filePath) {
|
||||
const raw = fs.readFileSync(filePath, "utf8");
|
||||
const value = stripFrontmatter(raw);
|
||||
await compile(
|
||||
{ path: filePath, value },
|
||||
{
|
||||
development: false,
|
||||
jsx: false,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function findDocsJsonPaths(roots) {
|
||||
const paths = new Set();
|
||||
for (const root of roots) {
|
||||
const absolute = path.resolve(root);
|
||||
if (!fs.existsSync(absolute)) {
|
||||
continue;
|
||||
}
|
||||
const stat = fs.statSync(absolute);
|
||||
if (stat.isFile() && path.basename(absolute) === "docs.json") {
|
||||
paths.add(absolute);
|
||||
continue;
|
||||
}
|
||||
if (stat.isDirectory()) {
|
||||
const docsJsonPath = path.join(absolute, "docs.json");
|
||||
if (fs.existsSync(docsJsonPath)) {
|
||||
paths.add(docsJsonPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
return [...paths];
|
||||
}
|
||||
|
||||
function collectNavigationLanguages(value, out = []) {
|
||||
if (Array.isArray(value)) {
|
||||
for (const item of value) {
|
||||
collectNavigationLanguages(item, out);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
if (!value || typeof value !== "object") {
|
||||
return out;
|
||||
}
|
||||
if (typeof value.language === "string") {
|
||||
out.push(value.language);
|
||||
}
|
||||
for (const child of Object.values(value)) {
|
||||
if (child && typeof child === "object") {
|
||||
collectNavigationLanguages(child, out);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function checkDocsJson(filePath) {
|
||||
const errors = [];
|
||||
let data;
|
||||
try {
|
||||
data = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
||||
} catch (error) {
|
||||
return [
|
||||
{
|
||||
type: "docs-json",
|
||||
file: filePath,
|
||||
message: `Invalid JSON: ${String(error?.message ?? error)}`,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
const languages = collectNavigationLanguages(data?.navigation);
|
||||
for (const language of languages) {
|
||||
if (!MINTLIFY_LANGUAGE_CODES.has(language)) {
|
||||
errors.push({
|
||||
type: "docs-json",
|
||||
file: filePath,
|
||||
message: `Unsupported Mintlify navigation language: ${language}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
|
||||
function relativize(root, filePath) {
|
||||
const relative = path.relative(root, filePath);
|
||||
return relative && !relative.startsWith("..") ? relative : filePath;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const startedAt = Date.now();
|
||||
const args = parseArgs(process.argv.slice(2));
|
||||
const cwd = process.cwd();
|
||||
const roots = args.roots.map((root) => path.resolve(root));
|
||||
const files = [
|
||||
...new Set(
|
||||
roots.flatMap((root) => {
|
||||
if (!fs.existsSync(root)) {
|
||||
throw new Error(`path does not exist: ${root}`);
|
||||
}
|
||||
return walkMarkdownFiles(root);
|
||||
}),
|
||||
),
|
||||
].toSorted((left, right) => left.localeCompare(right));
|
||||
|
||||
const errors = [];
|
||||
for (const docsJsonPath of findDocsJsonPaths(args.roots)) {
|
||||
errors.push(...checkDocsJson(docsJsonPath));
|
||||
}
|
||||
|
||||
for (const file of files) {
|
||||
try {
|
||||
await checkMdxFile(file);
|
||||
} catch (error) {
|
||||
errors.push(formatMdxError(file, error));
|
||||
if (errors.length >= args.maxErrors) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const report = {
|
||||
files: files.length,
|
||||
errors: errors.map((error) => Object.assign({}, error, { file: relativize(cwd, error.file) })),
|
||||
ms: Date.now() - startedAt,
|
||||
};
|
||||
|
||||
if (args.jsonOut) {
|
||||
fs.mkdirSync(path.dirname(path.resolve(args.jsonOut)), { recursive: true });
|
||||
fs.writeFileSync(args.jsonOut, `${JSON.stringify(report, null, 2)}\n`);
|
||||
}
|
||||
|
||||
if (report.errors.length === 0) {
|
||||
console.log(`Docs MDX check passed (${report.files} files, ${report.ms}ms).`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.error(`Docs MDX check failed (${report.errors.length} error(s), ${report.files} files).`);
|
||||
for (const error of report.errors) {
|
||||
const location =
|
||||
error.line && error.column ? `${error.file}:${error.line}:${error.column}` : error.file;
|
||||
console.error(`- ${location}: ${error.message}`);
|
||||
}
|
||||
process.exitCode = 1;
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error?.stack ?? error);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -9,6 +9,16 @@ const HERE = path.dirname(fileURLToPath(import.meta.url));
|
||||
const ROOT = path.resolve(HERE, "..");
|
||||
const SOURCE_DOCS_DIR = path.join(ROOT, "docs");
|
||||
const SOURCE_CONFIG_PATH = path.join(SOURCE_DOCS_DIR, "docs.json");
|
||||
const SYNC_SUPPORT_FILES = [
|
||||
{
|
||||
source: path.join(ROOT, "scripts", "check-docs-mdx.mjs"),
|
||||
target: path.join(".openclaw-sync", "check-docs-mdx.mjs"),
|
||||
},
|
||||
{
|
||||
source: path.join(ROOT, ".github", "codex", "prompts", "docs-mdx-repair.md"),
|
||||
target: path.join(".openclaw-sync", "docs-mdx-repair.md"),
|
||||
},
|
||||
];
|
||||
const GENERATED_LOCALES = [
|
||||
{
|
||||
language: "zh-Hans",
|
||||
@@ -107,6 +117,7 @@ const GENERATED_LOCALES = [
|
||||
navFile: "th-navigation.json",
|
||||
tmFile: "th.tm.jsonl",
|
||||
navMode: "clone-en",
|
||||
navigation: false,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -228,10 +239,14 @@ function composeDocsConfig() {
|
||||
}
|
||||
|
||||
const englishNav = languages.find((entry) => entry?.language === "en");
|
||||
const generatedLanguageSet = new Set(GENERATED_LOCALES.map((entry) => entry.language));
|
||||
const generatedLanguageSet = new Set(
|
||||
GENERATED_LOCALES.filter((entry) => entry.navigation !== false).map((entry) => entry.language),
|
||||
);
|
||||
const withoutGenerated = languages.filter((entry) => !generatedLanguageSet.has(entry?.language));
|
||||
const enIndex = withoutGenerated.findIndex((entry) => entry?.language === "en");
|
||||
const generated = GENERATED_LOCALES.map((entry) => composeLocaleNav(entry, englishNav));
|
||||
const generated = GENERATED_LOCALES.filter((entry) => entry.navigation !== false).map((entry) =>
|
||||
composeLocaleNav(entry, englishNav),
|
||||
);
|
||||
if (enIndex === -1) {
|
||||
withoutGenerated.push(...generated);
|
||||
} else {
|
||||
@@ -295,6 +310,14 @@ function writeSyncMetadata(targetRoot, args) {
|
||||
writeJson(path.join(targetRoot, ".openclaw-sync", "source.json"), metadata);
|
||||
}
|
||||
|
||||
function syncSupportFiles(targetRoot) {
|
||||
for (const entry of SYNC_SUPPORT_FILES) {
|
||||
const targetPath = path.join(targetRoot, entry.target);
|
||||
ensureDir(path.dirname(targetPath));
|
||||
fs.copyFileSync(entry.source, targetPath);
|
||||
}
|
||||
}
|
||||
|
||||
function main() {
|
||||
const args = parseArgs(process.argv.slice(2));
|
||||
const targetRoot = path.resolve(args.target);
|
||||
@@ -304,6 +327,7 @@ function main() {
|
||||
}
|
||||
|
||||
syncDocsTree(targetRoot);
|
||||
syncSupportFiles(targetRoot);
|
||||
writeSyncMetadata(targetRoot, args);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user