blob: d6981f121a7a21edcee323e40a669287f7d509a5 [file]
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const { buildManifest } = require('./manifest');
const { lintFrontmatter } = require('./lint-frontmatter');
const { lintMarkdownStructure } = require('./lint-markdown-structure');
const { lintSidebar } = require('./lint-sidebar');
const { lintLinks } = require('./lint-links');
const { lintSeo } = require('./lint-seo');
const { lintSqlFunctionDocs } = require('./lint-sql-function-docs');
const { lintFeatureDocs } = require('./lint-feature-docs');
const { lintI18nSync } = require('./lint-i18n-sync');
const { ensureDirForFile, getChangedFiles, getChangedRecords, parseArgs } = require('./lib');
function filterFindings(findings, changedFiles) {
if (!changedFiles || changedFiles.length === 0) {
return findings;
}
const changed = new Set(changedFiles);
return findings.filter((finding) => {
const related = finding.related_paths || [];
return (
changed.has(finding.path) ||
changed.has(finding.path.replace(/^\.\//, '')) ||
related.some((relatedPath) => changed.has(relatedPath))
);
});
}
function runChecks(options = {}) {
const rootDir = options.rootDir || process.cwd();
const changedFiles = options.changedFiles || null;
const changedRecords = options.changedRecords || [];
const manifest = buildManifest({ rootDir });
const findings = filterFindings(
[
...lintFrontmatter({ rootDir, manifest }),
...lintMarkdownStructure({ rootDir, manifest }),
...lintSidebar({ rootDir, manifest }),
...lintLinks({ rootDir, manifest, changedFiles: changedFiles || [], changedRecords }),
...lintSeo({ rootDir, manifest }),
...lintSqlFunctionDocs({ rootDir, manifest }),
...lintFeatureDocs({ rootDir, manifest }),
...lintI18nSync({ rootDir, manifest, changedFiles }),
],
changedFiles,
);
const bySeverity = findings.reduce((acc, finding) => {
acc[finding.severity] = (acc[finding.severity] || 0) + 1;
return acc;
}, {});
return {
schema_version: 1,
generated_at: new Date().toISOString(),
summary: {
entries: manifest.entries.length,
findings: findings.length,
by_severity: bySeverity,
changed_only: Boolean(changedFiles),
},
manifest,
findings,
};
}
function escapeAnnotation(value) {
return String(value)
.replace(/%/g, '%25')
.replace(/\r/g, '%0D')
.replace(/\n/g, '%0A')
.replace(/:/g, '%3A')
.replace(/,/g, '%2C');
}
function printGithubAnnotations(findings) {
for (const finding of findings) {
const level = finding.severity === 'error'
? 'error'
: finding.severity === 'info'
? 'notice'
: 'warning';
const file = escapeAnnotation(finding.path);
const line = finding.line || 1;
const title = escapeAnnotation(finding.rule);
const message = escapeAnnotation(`${finding.message} Owner: ${finding.owner || 'unknown'}`);
process.stdout.write(`::${level} file=${file},line=${line},title=${title}::${message}\n`);
}
}
function runCli() {
const args = parseArgs(process.argv.slice(2));
const rootDir = args.root ? path.resolve(args.root) : process.cwd();
const changedFiles = args.changed ? getChangedFiles(rootDir) : args.files ? args.files.split(',') : null;
const changedRecords = args.changed ? getChangedRecords(rootDir) : [];
const report = runChecks({ rootDir, changedFiles, changedRecords });
const format = args.format || 'github';
if (format === 'github') {
printGithubAnnotations(report.findings);
process.stdout.write(
`Docs governance report: ${report.summary.findings} findings across ${report.summary.entries} manifest entries.\n`,
);
} else {
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
}
if (args.output) {
const outputPath = path.resolve(rootDir, args.output);
ensureDirForFile(outputPath);
fs.writeFileSync(outputPath, `${JSON.stringify(report, null, 2)}\n`, 'utf8');
}
if (args['fail-on-findings'] && report.findings.length > 0) {
process.exitCode = 1;
}
}
if (require.main === module) {
runCli();
}
module.exports = {
escapeAnnotation,
printGithubAnnotations,
runChecks,
};