blob: ad03133137c89c347ae21159b351c6624bc96698 [file]
const fs = require('fs');
const path = require('path');
const yaml = require('js-yaml');
const DOC_SUFFIX_RE = /\.(md|mdx)$/i;
const DEFAULT_ACTIVE_VERSIONS = ['4.x', '3.x', '2.1', 'current'];
const DEFAULT_ARCHIVED_VERSIONS = ['2.0', '1.2'];
const DEFAULT_VERSION = '4.x';
const CURRENT_ROUTE_VERSION = 'dev';
function normalizePath(filePath) {
return filePath.replace(/\\/g, '/').replace(/^\.\/+/, '');
}
function toPosixPath(filePath) {
return normalizePath(filePath.split(path.sep).join('/'));
}
function stripMarkdownExtension(filePath) {
return filePath.replace(DOC_SUFFIX_RE, '');
}
function isMarkdownFile(filePath) {
return DOC_SUFFIX_RE.test(filePath);
}
function fileExists(rootDir, relativePath) {
return fs.existsSync(path.join(rootDir, relativePath));
}
function readYamlIfExists(filePath, fallback) {
if (!fs.existsSync(filePath)) {
return fallback;
}
return yaml.load(fs.readFileSync(filePath, 'utf8')) || fallback;
}
function loadRules(rootDir) {
const rules = readYamlIfExists(path.join(rootDir, '.docs-governance', 'rules.yml'), {});
const versionsJson = path.join(rootDir, 'versions.json');
const activeVersions = fs.existsSync(versionsJson)
? JSON.parse(fs.readFileSync(versionsJson, 'utf8'))
: rules?.versions?.active || DEFAULT_ACTIVE_VERSIONS;
return {
raw: rules,
activeVersions,
archivedVersions: rules?.versions?.archived || DEFAULT_ARCHIVED_VERSIONS,
defaultVersion: rules?.versions?.default_version || DEFAULT_VERSION,
currentRouteVersion: rules?.versions?.current_route_version || CURRENT_ROUTE_VERSION,
docTypeRules: rules?.doc_type_detection?.ordered_rules || [],
};
}
function loadOwners(rootDir) {
const owners = readYamlIfExists(path.join(rootDir, '.docs-governance', 'owners.yml'), {});
return {
defaultOwner: owners.default_owner || '@apache/doris-website-maintainers',
owners: owners.owners || [],
};
}
function loadExceptions(rootDir) {
const exceptions = readYamlIfExists(path.join(rootDir, '.docs-governance', 'exceptions.yml'), {});
return exceptions.exceptions || [];
}
function walkFiles(rootDir, predicate = () => true) {
const result = [];
function visit(current) {
if (!fs.existsSync(current)) {
return;
}
const stat = fs.statSync(current);
if (stat.isDirectory()) {
for (const entry of fs.readdirSync(current).sort()) {
if (entry === 'node_modules' || entry === '.git' || entry === 'build') {
continue;
}
visit(path.join(current, entry));
}
return;
}
if (stat.isFile() && predicate(current)) {
result.push(current);
}
}
visit(rootDir);
return result;
}
function walkMarkdownFiles(rootDir, roots) {
const result = [];
for (const root of roots) {
const absRoot = path.join(rootDir, root);
const files = walkFiles(absRoot, (filePath) => isMarkdownFile(filePath));
result.push(...files);
}
return result;
}
function globToRegExp(glob) {
let source = '^';
for (let i = 0; i < glob.length; i += 1) {
const char = glob[i];
const next = glob[i + 1];
if (char === '*' && next === '*') {
source += '.*';
i += 1;
} else if (char === '*') {
source += '[^/]*';
} else {
source += char.replace(/[.+?^${}()|[\]\\]/g, '\\$&');
}
}
source += '$';
return new RegExp(source);
}
function matchesGlob(filePath, glob) {
return globToRegExp(glob).test(normalizePath(filePath));
}
function parseArgs(argv) {
const args = {};
for (let i = 0; i < argv.length; i += 1) {
const arg = argv[i];
if (!arg.startsWith('--')) {
continue;
}
const key = arg.slice(2);
const next = argv[i + 1];
if (!next || next.startsWith('--')) {
args[key] = true;
} else {
args[key] = next;
i += 1;
}
}
return args;
}
function getChangedFiles(rootDir) {
const { execSync } = require('child_process');
if (!process.env.GITHUB_BASE_REF) {
return execSync('git diff --name-only HEAD', {
cwd: rootDir,
encoding: 'utf8',
})
.split(/\r?\n/)
.map((item) => item.trim())
.filter(Boolean);
}
const baseRef = `origin/${process.env.GITHUB_BASE_REF}`;
try {
const base = execSync(`git merge-base ${baseRef} HEAD`, {
cwd: rootDir,
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'ignore'],
}).trim();
return execSync(`git diff --name-only ${base} HEAD`, {
cwd: rootDir,
encoding: 'utf8',
})
.split(/\r?\n/)
.map((item) => item.trim())
.filter(Boolean);
} catch (err) {
return execSync('git diff --name-only HEAD', {
cwd: rootDir,
encoding: 'utf8',
})
.split(/\r?\n/)
.map((item) => item.trim())
.filter(Boolean);
}
}
function parseNameStatus(raw) {
return raw
.split(/\r?\n/)
.map((line) => line.trim())
.filter(Boolean)
.map((line) => {
const parts = line.split(/\t+/);
const status = parts[0];
if (status.startsWith('R') || status.startsWith('C')) {
return { status: status[0], oldPath: normalizePath(parts[1] || ''), path: normalizePath(parts[2] || '') };
}
return { status: status[0], path: normalizePath(parts[1] || '') };
})
.filter((record) => record.path || record.oldPath);
}
function getChangedRecords(rootDir) {
const { execSync } = require('child_process');
const fallback = () => parseNameStatus(execSync('git diff --name-status HEAD', {
cwd: rootDir,
encoding: 'utf8',
}));
if (!process.env.GITHUB_BASE_REF) {
return fallback();
}
const baseRef = `origin/${process.env.GITHUB_BASE_REF}`;
try {
const base = execSync(`git merge-base ${baseRef} HEAD`, {
cwd: rootDir,
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'ignore'],
}).trim();
return parseNameStatus(execSync(`git diff --name-status ${base} HEAD`, {
cwd: rootDir,
encoding: 'utf8',
}));
} catch (err) {
return fallback();
}
}
function ensureDirForFile(filePath) {
fs.mkdirSync(path.dirname(filePath), { recursive: true });
}
module.exports = {
DEFAULT_ACTIVE_VERSIONS,
DEFAULT_ARCHIVED_VERSIONS,
DEFAULT_VERSION,
CURRENT_ROUTE_VERSION,
DOC_SUFFIX_RE,
ensureDirForFile,
fileExists,
getChangedFiles,
getChangedRecords,
isMarkdownFile,
loadExceptions,
loadOwners,
loadRules,
matchesGlob,
normalizePath,
parseArgs,
stripMarkdownExtension,
toPosixPath,
walkFiles,
walkMarkdownFiles,
};