Files
OmniRoute/scripts/check-docs-sync.mjs
2026-05-10 00:00:11 -03:00

175 lines
4.9 KiB
JavaScript

#!/usr/bin/env node
import fs from "node:fs";
import path from "node:path";
const cwd = process.cwd();
const packageJsonPath = path.resolve(cwd, "package.json");
const openApiPath = path.resolve(cwd, "docs/openapi.yaml");
const changelogPath = path.resolve(cwd, "CHANGELOG.md");
const llmPath = path.resolve(cwd, "llm.txt");
const i18nDocsPath = path.resolve(cwd, "docs/i18n");
function readText(filePath) {
if (!fs.existsSync(filePath)) {
throw new Error(`File not found: ${path.relative(cwd, filePath)}`);
}
return fs.readFileSync(filePath, "utf8");
}
function extractOpenApiVersion(content) {
const lines = content.split(/\r?\n/);
let inInfoBlock = false;
for (const line of lines) {
const trimmed = line.trim();
if (!inInfoBlock) {
if (trimmed === "info:") {
inInfoBlock = true;
}
continue;
}
if (line.length > 0 && !line.startsWith(" ")) {
break;
}
const match = line.match(/^\s{2}version:\s*["']?([^"'\s]+)["']?\s*$/);
if (match) {
return match[1];
}
}
return null;
}
function extractChangelogSections(content) {
const headings = [...content.matchAll(/^##\s+\[([^\]]+)\](?:\s+[-—–].*)?$/gm)];
return headings.map((match) => match[1]);
}
function stripTopHeading(content) {
return content.replace(/^# .+\r?\n+/, "");
}
function extractI18nMirrorBody(content) {
const separator = content.match(/^---\s*$/m);
if (!separator || separator.index === undefined) {
return null;
}
return content.slice(separator.index + separator[0].length).replace(/^\r?\n+/, "");
}
function normalizeMirrorBody(content) {
return content.replace(/\r\n/g, "\n").trim();
}
function isSemver(value) {
// Accept X.Y.Z and X.Y.Z-prerelease.N (e.g. 3.0.0-rc.1, 3.0.0-beta.2)
return /^\d+\.\d+\.\d+(-[a-zA-Z0-9.]+)?$/.test(value);
}
let hasFailure = false;
function fail(message) {
hasFailure = true;
console.error(`[docs-sync] FAIL - ${message}`);
}
function checkI18nMirrorFile(fileName, sourcePath) {
if (!fs.existsSync(i18nDocsPath)) {
fail("docs/i18n directory is missing");
return;
}
const sourceBody = normalizeMirrorBody(stripTopHeading(readText(sourcePath)));
const locales = fs
.readdirSync(i18nDocsPath, { withFileTypes: true })
.filter((entry) => entry.isDirectory())
.map((entry) => entry.name)
.sort();
let checked = 0;
for (const locale of locales) {
const targetPath = path.join(i18nDocsPath, locale, fileName);
if (!fs.existsSync(targetPath)) {
fail(`docs/i18n/${locale}/${fileName} is missing`);
continue;
}
const body = extractI18nMirrorBody(readText(targetPath));
if (body === null) {
fail(`docs/i18n/${locale}/${fileName} is missing the i18n mirror separator`);
continue;
}
if (normalizeMirrorBody(body) !== sourceBody) {
fail(`docs/i18n/${locale}/${fileName} differs from root ${fileName}`);
continue;
}
checked += 1;
}
if (checked > 0) {
console.log(`[docs-sync] ${fileName} i18n mirrors match root content: ${checked} locales`);
}
}
try {
const packageJson = JSON.parse(readText(packageJsonPath));
const packageVersion = packageJson.version;
if (!isSemver(packageVersion)) {
fail(`package.json version is not valid semver: "${packageVersion}"`);
} else {
console.log(`[docs-sync] package.json version: ${packageVersion}`);
}
const openApiVersion = extractOpenApiVersion(readText(openApiPath));
if (!openApiVersion) {
fail("could not extract docs/openapi.yaml info.version");
} else if (openApiVersion !== packageVersion) {
fail(`OpenAPI version (${openApiVersion}) differs from package.json (${packageVersion})`);
} else {
console.log(`[docs-sync] openapi.yaml info.version matches: ${openApiVersion}`);
}
const changelogSections = extractChangelogSections(readText(changelogPath));
if (changelogSections.length === 0) {
fail("CHANGELOG.md has no version sections");
} else {
if (changelogSections[0] !== "Unreleased") {
fail('CHANGELOG.md first section must be "## [Unreleased]"');
} else {
console.log("[docs-sync] changelog has top Unreleased section");
}
const semverSections = changelogSections.filter((section) => isSemver(section));
if (semverSections.length === 0) {
fail("CHANGELOG.md has no semver release section");
} else if (semverSections[0] !== packageVersion) {
fail(
`Latest changelog release (${semverSections[0]}) differs from package.json (${packageVersion})`
);
} else {
console.log(
`[docs-sync] latest changelog release matches package version: ${packageVersion}`
);
}
}
checkI18nMirrorFile("llm.txt", llmPath);
checkI18nMirrorFile("CHANGELOG.md", changelogPath);
} catch (error) {
fail(error instanceof Error ? error.message : String(error));
}
if (hasFailure) {
process.exit(1);
}
console.log("[docs-sync] PASS - documentation version sync is consistent.");