| 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', 'current']; | |
| const DEFAULT_ARCHIVED_VERSIONS = ['2.1', '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, | |
| }; |