docs-v2/scripts/docs-cli/commands/audit.js

471 lines
15 KiB
JavaScript

#!/usr/bin/env node
/**
* Documentation audit command
*
* SECURITY: This file must NOT contain references to private repository names or URLs
* All sensitive configuration is user-provided via environment variables
*/
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
import { existsSync } from 'fs';
import {
checkGitHubAuth,
getConfig,
hasEnterpriseAccess,
getRepoPathOrClone,
cloneOrUpdateRepo,
} from '../lib/config-loader.js';
import {
resolveProducts,
validateMutualExclusion,
getProductInfo,
} from '../lib/product-resolver.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// Map product keys to audit product types
const PRODUCT_KEY_TO_AUDIT_TYPE = {
influxdb3_core: 'core',
influxdb3_enterprise: 'enterprise',
telegraf: 'telegraf',
};
function printUsage() {
console.log(`
Documentation Coverage Audit
Usage: docs audit [options]
Options:
--products <keys> Product keys or content paths (comma-separated)
Examples: influxdb3_core, /influxdb3/core
--repos <paths> Direct repo paths or URLs (alternative to --products)
--version <v> Version/branch/tag to audit (default: main)
--categories <list> Comma-separated categories to audit
--branch <name> docs-v2 branch to compare against (default: master)
--output-format <fmt> Output format: report | drafts | json (default: report)
--help, -h Show this help message
Note: --products and --repos are mutually exclusive.
Product Keys (or use content paths like /influxdb3/core):
influxdb3_core InfluxDB 3 Core
influxdb3_enterprise InfluxDB 3 Enterprise
telegraf Telegraf
Available Categories:
CLI_REFERENCE CLI command documentation
API_REFERENCE HTTP API endpoint documentation
GETTING_STARTED Getting started guides
ADMIN_GUIDES Administration documentation
WRITE_DATA Write data guides
QUERY_DATA Query data guides
PROCESS_DATA Process data guides
GENERAL_REFERENCE General reference documentation
Examples:
# Audit using product keys (version defaults to main)
docs audit --products influxdb3_core
# Audit using content paths
docs audit --products /influxdb3/core,/influxdb3/enterprise
# Audit specific version
docs audit --products influxdb3_core --version v3.3.0
# Audit using direct repo path
docs audit --repos ~/github/influxdata/influxdb
# Audit using repo URL (will clone automatically)
docs audit --repos https://github.com/influxdata/telegraf
# Audit with specific categories
docs audit --products influxdb3_core --categories CLI_REFERENCE
Configuration:
Requires GitHub CLI authentication: gh auth login
For Enterprise audits:
1. Ensure you have access to Enterprise repositories on GitHub
2. Configure in ~/.influxdata-docs/docs-cli.yml:
repositories:
influxdb3_enterprise:
path: ~/github/influxdata/<enterprise-repo>
See scripts/docs-cli/config/README.md for full configuration options
`);
}
function validatePrerequisites(product) {
const auth = checkGitHubAuth();
if (!auth.authenticated) {
console.error('Error: GitHub CLI not authenticated');
console.error('');
console.error('Run: gh auth login');
console.error('');
process.exit(1);
}
if (
(product === 'enterprise' || product === 'both') &&
!hasEnterpriseAccess()
) {
console.error('Warning: Enterprise audit requires configuration');
console.error('');
console.error('To audit Enterprise products:');
console.error(
'1. Ensure you have GitHub access to Enterprise repositories'
);
console.error('2. Add to .env file:');
console.error(' DOCS_ENTERPRISE_ACCESS=true');
console.error(' DOCS_ENTERPRISE_REPO_URL=<enterprise-repo-url>');
console.error('');
console.error('See config/README.md for details');
console.error('');
process.exit(1);
}
console.log('✓ GitHub CLI authenticated');
if (product === 'enterprise' || product === 'both') {
console.log('✓ Enterprise access configured');
}
console.log('');
}
function getRepoURL(envKey, publicDefault) {
return getConfig(envKey, { defaultValue: publicDefault });
}
export default async function audit(args) {
const positionals = args.args || [];
if (positionals.includes('--help') || positionals.includes('-h')) {
printUsage();
process.exit(0);
}
// Parse arguments
let productsInput = null;
let reposInput = null;
let version = 'main'; // Default to main
let categoryFilter = null;
let docsBranch = 'master';
let outputFormat = 'report';
for (let i = 0; i < positionals.length; i++) {
const arg = positionals[i];
if (arg === '--products' || arg === '--product') {
if (i + 1 < positionals.length && !positionals[i + 1].startsWith('--')) {
productsInput = positionals[i + 1];
i++;
} else {
console.error('Error: --products requires product keys or paths');
process.exit(1);
}
} else if (arg.startsWith('--products=')) {
productsInput = arg.split('=')[1];
} else if (arg === '--repos' || arg === '--repo') {
if (i + 1 < positionals.length && !positionals[i + 1].startsWith('--')) {
reposInput = positionals[i + 1];
i++;
} else {
console.error('Error: --repos requires paths or URLs');
process.exit(1);
}
} else if (arg.startsWith('--repos=')) {
reposInput = arg.split('=')[1];
} else if (arg === '--version') {
if (i + 1 < positionals.length && !positionals[i + 1].startsWith('--')) {
version = positionals[i + 1];
i++;
} else {
console.error('Error: --version requires a value');
process.exit(1);
}
} else if (arg.startsWith('--version=')) {
version = arg.split('=')[1];
} else if (arg === '--categories') {
if (i + 1 < positionals.length && !positionals[i + 1].startsWith('--')) {
categoryFilter = positionals[i + 1]
.split(',')
.map((c) => c.trim().toUpperCase());
i++;
}
} else if (arg.startsWith('--categories=')) {
categoryFilter = arg
.split('=')[1]
.split(',')
.map((c) => c.trim().toUpperCase());
} else if (arg === '--branch') {
if (i + 1 < positionals.length && !positionals[i + 1].startsWith('--')) {
docsBranch = positionals[i + 1];
i++;
}
} else if (arg.startsWith('--branch=')) {
docsBranch = arg.split('=')[1];
} else if (arg === '--output-format') {
if (i + 1 < positionals.length && !positionals[i + 1].startsWith('--')) {
outputFormat = positionals[i + 1];
i++;
}
} else if (arg.startsWith('--output-format=')) {
outputFormat = arg.split('=')[1];
} else if (!arg.startsWith('--')) {
// Positional arguments are no longer supported
console.error(`Error: Unexpected positional argument '${arg}'`);
console.error('');
console.error('Positional arguments are no longer supported. Use flags:');
console.error(' docs audit --products influxdb3_core --version v3.9');
console.error('');
console.error('Quick migration:');
console.error(' core → --products influxdb3_core');
console.error(' enterprise → --products influxdb3_enterprise');
console.error(' telegraf → --products telegraf');
console.error('');
console.error("Run 'docs audit --help' for usage information.");
process.exit(1);
}
}
// Validate mutual exclusion
validateMutualExclusion({ products: productsInput, repos: reposInput });
// Validate output format
if (!['report', 'drafts', 'json'].includes(outputFormat)) {
console.error(`Error: Invalid output format '${outputFormat}'`);
console.error('Must be one of: report, drafts, json');
process.exit(1);
}
// Validate we have products or repos to audit
if (!productsInput && !reposInput) {
console.error('Error: No products or repositories specified');
console.error('');
console.error('Options:');
console.error(' --products <keys> Product keys or content paths');
console.error(
' Examples: influxdb3_core, /influxdb3/core'
);
console.error(' --repos <paths> Direct repository paths or URLs');
console.error('');
console.error('Available products:');
for (const key of Object.keys(PRODUCT_KEY_TO_AUDIT_TYPE)) {
const info = getProductInfo(key);
const path = info?.contentPath ? ` (/${info.contentPath}/)` : '';
console.error(` ${key}${path}`);
}
console.error('');
console.error("Run 'docs audit --help' for full usage information.");
process.exit(1);
}
// Resolve product keys
let productKeys = [];
let repoPaths = [];
if (productsInput) {
try {
const resolved = resolveProducts(productsInput);
productKeys = resolved.map((r) => r.key);
// Filter to only auditable products
const auditableKeys = productKeys.filter(
(key) => PRODUCT_KEY_TO_AUDIT_TYPE[key]
);
const nonAuditableKeys = productKeys.filter(
(key) => !PRODUCT_KEY_TO_AUDIT_TYPE[key]
);
if (nonAuditableKeys.length > 0) {
console.warn(
`Warning: Skipping non-auditable products: ${nonAuditableKeys.join(', ')}`
);
console.warn(
'Auditing is only supported for: influxdb3_core, influxdb3_enterprise, telegraf'
);
console.warn('');
}
if (auditableKeys.length === 0) {
console.error('Error: No auditable products specified');
console.error(
'Auditing is only supported for: influxdb3_core, influxdb3_enterprise, telegraf'
);
process.exit(1);
}
productKeys = auditableKeys;
console.log(`✓ Resolved products: ${productKeys.join(', ')}`);
} catch (error) {
console.error(error.message);
process.exit(1);
}
}
if (reposInput) {
repoPaths = reposInput.split(',').map((p) => p.trim());
}
console.log(`✓ Version: ${version}`);
console.log(`✓ Docs branch: ${docsBranch}`);
console.log('');
// Convert product keys to audit product types
const auditProducts = [
...new Set(productKeys.map((k) => PRODUCT_KEY_TO_AUDIT_TYPE[k])),
];
// Check if auditing both core and enterprise
let hasCore = auditProducts.includes('core');
let hasEnterprise = auditProducts.includes('enterprise');
let hasTelegraf = auditProducts.includes('telegraf');
// Validate prerequisites for products (skip for direct repos)
if (productKeys.length > 0) {
for (const product of auditProducts) {
validatePrerequisites(product);
}
}
// Set repository URLs/paths from --products
for (const productKey of productKeys) {
const repoPath = await getRepoPathOrClone(productKey, {
allowClone: true,
fetch: true,
});
if (productKey === 'influxdb3_core') {
process.env.INFLUXDB_REPO_URL =
repoPath ||
getRepoURL(
'DOCS_CORE_REPO_URL',
'https://github.com/influxdata/influxdb.git'
);
} else if (productKey === 'influxdb3_enterprise') {
process.env.INFLUXDB3_ENTERPRISE_REPO_URL =
repoPath || getRepoURL('DOCS_ENTERPRISE_REPO_URL', '');
} else if (productKey === 'telegraf') {
process.env.TELEGRAF_REPO_URL =
repoPath ||
getRepoURL(
'DOCS_TELEGRAF_REPO_URL',
'https://github.com/influxdata/telegraf.git'
);
}
}
// Handle --repos: direct paths or URLs
if (repoPaths.length > 0) {
for (const pathOrUrl of repoPaths) {
let repoPath = pathOrUrl;
const repoName = pathOrUrl
.split('/')
.pop()
.replace(/\.git$/, '');
// Check if it's a URL (needs cloning)
if (
pathOrUrl.startsWith('http://') ||
pathOrUrl.startsWith('https://') ||
pathOrUrl.startsWith('git@')
) {
repoPath = await cloneOrUpdateRepo(pathOrUrl, repoName, {
fetch: true,
});
} else if (!existsSync(pathOrUrl)) {
console.error(`Error: Repository path not found: ${pathOrUrl}`);
process.exit(1);
}
// Detect repo type from path/name and set appropriate env var
const lowerName = repoName.toLowerCase();
if (lowerName.includes('telegraf')) {
process.env.TELEGRAF_REPO_URL = repoPath;
hasTelegraf = true;
} else if (
lowerName.includes('enterprise') ||
lowerName.includes('pro')
) {
process.env.INFLUXDB3_ENTERPRISE_REPO_URL = repoPath;
hasEnterprise = true;
} else {
// Default to core/influxdb
process.env.INFLUXDB_REPO_URL = repoPath;
hasCore = true;
}
}
}
process.env.DOCS_V2_REPO_URL = getRepoURL(
'DOCS_REPO_URL',
'https://github.com/influxdata/docs-v2.git'
);
// Determine which audits to run
const runCLIAudit =
(hasCore || hasEnterprise) &&
(!categoryFilter ||
categoryFilter.includes('CLI_REFERENCE') ||
categoryFilter.some((c) => c !== 'API_REFERENCE'));
const runAPIAudit =
(hasCore || hasEnterprise) &&
(!categoryFilter || categoryFilter.includes('API_REFERENCE'));
try {
// Run Telegraf audit if requested
if (hasTelegraf) {
console.log('📋 Running Telegraf audit...\n');
const { runTelegrafAudit } = await import('../lib/telegraf-auditor.js');
await runTelegrafAudit(version, docsBranch, outputFormat);
}
// Run InfluxDB audits
if (hasCore || hasEnterprise) {
// Determine audit product type
const influxProduct =
hasCore && hasEnterprise
? 'both'
: hasEnterprise
? 'enterprise'
: 'core';
if (runCLIAudit) {
console.log(`📋 Running CLI audit for ${influxProduct}...\n`);
const auditorPath = join(
__dirname,
'../../influxdb3-monolith/cli-docs-audit/documentation-audit.js'
);
const { CLIDocumentationAuditor } = await import(auditorPath);
const cliAuditor = new CLIDocumentationAuditor(
influxProduct,
version,
categoryFilter,
docsBranch
);
cliAuditor.outputFormat = outputFormat;
await cliAuditor.run();
}
if (runAPIAudit) {
console.log(`📋 Running API audit for ${influxProduct}...\n`);
const { runAPIAudit: runAPIAuditFn } = await import(
'../lib/api-auditor.js'
);
await runAPIAuditFn(influxProduct, version, docsBranch, outputFormat);
}
}
console.log('\n✅ Documentation audit complete!');
} catch (error) {
console.error('\n❌ Audit failed:', error.message);
if (process.env.DEBUG) {
console.error(error.stack);
}
process.exit(1);
}
}