From c74060210bbd0eb60b1c5e666622982d79bee974 Mon Sep 17 00:00:00 2001 From: Jason Stirnaman Date: Sat, 5 Jul 2025 15:24:08 -0500 Subject: [PATCH] chore(ci): Audit documentation for changes to influxdb3 CLI. Configures a GitHub release workflow to generate release notes and run the audit documentation script. --- .github/workflows/audit-documentation.yml | 41 +- .github/workflows/influxdb3-release.yml | 266 +++++ .github/workflows/trigger-on-release.yml | 61 ++ .../influxdb3-monolith/apply-cli-patches.js | 277 +++++ .../audit-cli-documentation.js | 960 ++++++++++++++++++ .../audit-cli-documentation.sh | 316 ------ package.json | 7 +- 7 files changed, 1605 insertions(+), 323 deletions(-) create mode 100644 .github/workflows/influxdb3-release.yml create mode 100644 .github/workflows/trigger-on-release.yml create mode 100755 helper-scripts/influxdb3-monolith/apply-cli-patches.js create mode 100755 helper-scripts/influxdb3-monolith/audit-cli-documentation.js delete mode 100755 helper-scripts/influxdb3-monolith/audit-cli-documentation.sh diff --git a/.github/workflows/audit-documentation.yml b/.github/workflows/audit-documentation.yml index 920eb3d3d..521a47a38 100644 --- a/.github/workflows/audit-documentation.yml +++ b/.github/workflows/audit-documentation.yml @@ -18,20 +18,37 @@ on: description: 'Version to audit (use "local" for running containers)' required: false default: 'local' + create_issue: + description: 'Create GitHub issue with audit results' + required: false + type: boolean + default: false schedule: # Run weekly on Mondays at 9 AM UTC + # Note: This only runs API audits for distributed products + # CLI audits for core/enterprise run via the release workflow - cron: '0 9 * * 1' jobs: audit-cli: name: Audit CLI Documentation runs-on: ubuntu-latest - if: contains(fromJSON('["core", "enterprise", "all-monolith"]'), github.event.inputs.product) + # Only run for manual triggers, not scheduled runs (which are for distributed products) + if: github.event_name == 'workflow_dispatch' && contains(fromJSON('["core", "enterprise", "all-monolith"]'), github.event.inputs.product) steps: - uses: actions/checkout@v4 + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '18' + cache: 'yarn' + + - name: Install dependencies + run: yarn install --frozen-lockfile + - name: Set up Docker if: github.event.inputs.version == 'local' run: | @@ -44,9 +61,9 @@ jobs: VERSION="${{ github.event.inputs.version }}" if [ "$PRODUCT" == "all-monolith" ]; then - ./helper-scripts/influxdb3-monolith/audit-cli-documentation.sh both $VERSION + node ./helper-scripts/influxdb3-monolith/audit-cli-documentation.js both $VERSION else - ./helper-scripts/influxdb3-monolith/audit-cli-documentation.sh $PRODUCT $VERSION + node ./helper-scripts/influxdb3-monolith/audit-cli-documentation.js $PRODUCT $VERSION fi - name: Upload CLI audit reports @@ -62,11 +79,23 @@ jobs: with: script: | const fs = require('fs'); - const product = '${{ github.event.inputs.product }}'; - const version = '${{ github.event.inputs.version }}'; + let product = '${{ github.event.inputs.product }}'; + let version = '${{ github.event.inputs.version }}'; + + // Handle scheduled runs (no inputs) + if (github.event_name === 'schedule') { + product = 'both'; + version = 'local'; + } // Read audit report const reportPath = `helper-scripts/output/cli-audit/documentation-audit-${product}-${version}.md`; + + if (!fs.existsSync(reportPath)) { + console.log(`Audit report not found at ${reportPath}`); + return; + } + const report = fs.readFileSync(reportPath, 'utf8'); // Create issue @@ -75,7 +104,7 @@ jobs: repo: context.repo.repo, title: `CLI Documentation Audit - ${product} ${version}`, body: report, - labels: ['documentation', 'cli-audit', product] + labels: ['documentation', 'cli-audit', product === 'both' ? 'core-enterprise' : product] }); audit-api: diff --git a/.github/workflows/influxdb3-release.yml b/.github/workflows/influxdb3-release.yml new file mode 100644 index 000000000..b03837e9e --- /dev/null +++ b/.github/workflows/influxdb3-release.yml @@ -0,0 +1,266 @@ +name: InfluxDB 3 Release Documentation + +on: + workflow_dispatch: + inputs: + product: + description: 'Product being released' + required: true + type: choice + options: + - core + - enterprise + - both + version: + description: 'Version being released (e.g., 3.0.0)' + required: true + type: string + previous_version: + description: 'Previous version for comparison (e.g., 2.9.0)' + required: true + type: string + dry_run: + description: 'Dry run (do not create PRs or issues)' + required: false + type: boolean + default: true + +jobs: + generate-release-notes: + name: Generate Release Notes + runs-on: ubuntu-latest + outputs: + release_notes_generated: ${{ steps.generate.outputs.generated }} + + steps: + - uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '18' + cache: 'yarn' + + - name: Install dependencies + run: yarn install --frozen-lockfile + + - name: Generate release notes + id: generate + run: | + echo "Generating release notes for ${{ github.event.inputs.product }} v${{ github.event.inputs.version }}" + + # TODO: Call the actual generate-release-notes script when it exists + # node ./helper-scripts/influxdb3-monolith/generate-release-notes.js \ + # --product ${{ github.event.inputs.product }} \ + # --version ${{ github.event.inputs.version }} \ + # --previous ${{ github.event.inputs.previous_version }} + + # For now, create a placeholder + mkdir -p helper-scripts/output/release-notes + echo "# Release Notes for ${{ github.event.inputs.product }} v${{ github.event.inputs.version }}" > helper-scripts/output/release-notes/release-notes-${{ github.event.inputs.product }}-${{ github.event.inputs.version }}.md + echo "Generated: true" >> $GITHUB_OUTPUT + + - name: Upload release notes + uses: actions/upload-artifact@v4 + with: + name: release-notes-${{ github.event.inputs.product }}-${{ github.event.inputs.version }} + path: helper-scripts/output/release-notes/ + retention-days: 30 + + audit-cli-documentation: + name: Audit CLI Documentation + needs: generate-release-notes + runs-on: ubuntu-latest + if: needs.generate-release-notes.outputs.release_notes_generated == 'true' + + steps: + - uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '18' + cache: 'yarn' + + - name: Install dependencies + run: yarn install --frozen-lockfile + + - name: Pull Docker images for version + run: | + VERSION="${{ github.event.inputs.version }}" + PRODUCT="${{ github.event.inputs.product }}" + + if [ "$PRODUCT" == "both" ]; then + docker pull influxdb:${VERSION}-core || true + docker pull influxdb:${VERSION}-enterprise || true + else + docker pull influxdb:${VERSION}-${PRODUCT} || true + fi + + - name: Run CLI audit + run: | + PRODUCT="${{ github.event.inputs.product }}" + VERSION="${{ github.event.inputs.version }}" + + node ./helper-scripts/influxdb3-monolith/audit-cli-documentation.js $PRODUCT $VERSION + + - name: Upload CLI audit reports + uses: actions/upload-artifact@v4 + with: + name: cli-audit-release-${{ github.event.inputs.product }}-${{ github.event.inputs.version }} + path: helper-scripts/output/cli-audit/ + retention-days: 90 + + create-documentation-pr: + name: Create Documentation PR + needs: [generate-release-notes, audit-cli-documentation] + runs-on: ubuntu-latest + if: github.event.inputs.dry_run != 'true' + + steps: + - uses: actions/checkout@v4 + + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + path: artifacts/ + + - name: Create release branch + run: | + BRANCH="release-docs-${{ github.event.inputs.product }}-${{ github.event.inputs.version }}" + git checkout -b $BRANCH + echo "BRANCH=$BRANCH" >> $GITHUB_ENV + + - name: Copy release notes to docs + run: | + # TODO: Copy release notes to appropriate documentation location + echo "Release notes would be copied here" + + - name: Create Pull Request + uses: peter-evans/create-pull-request@v5 + with: + token: ${{ secrets.GITHUB_TOKEN }} + branch: ${{ env.BRANCH }} + title: "docs: Release documentation for ${{ github.event.inputs.product }} v${{ github.event.inputs.version }}" + body: | + ## Release Documentation Update + + This PR contains documentation updates for **${{ github.event.inputs.product }} v${{ github.event.inputs.version }}** + + ### Included Updates: + - [ ] Release notes + - [ ] Version updates + - [ ] CLI documentation audit results + + ### Artifacts: + - [Release Notes](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}) + - [CLI Audit Report](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}) + + ### Manual Review Needed: + Please review the CLI audit report for any missing or outdated documentation that needs to be updated. + + --- + *This PR was automatically generated by the release workflow.* + labels: | + documentation + release + ${{ github.event.inputs.product }} + draft: true + + create-audit-issue: + name: Create CLI Audit Issue + needs: audit-cli-documentation + runs-on: ubuntu-latest + if: github.event.inputs.dry_run != 'true' + + steps: + - uses: actions/checkout@v4 + + - name: Download audit report + uses: actions/download-artifact@v4 + with: + name: cli-audit-release-${{ github.event.inputs.product }}-${{ github.event.inputs.version }} + path: audit-report/ + + - name: Create issue from audit + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + const product = '${{ github.event.inputs.product }}'; + const version = '${{ github.event.inputs.version }}'; + + // Find and read the audit report + const files = fs.readdirSync('audit-report'); + const auditFile = files.find(f => f.includes('documentation-audit')); + + if (!auditFile) { + console.log('No audit report found'); + return; + } + + const report = fs.readFileSync(`audit-report/${auditFile}`, 'utf8'); + + // Check if there are any issues to report + const hasMissingOptions = report.includes('âš ī¸ Missing from docs'); + const hasExtraOptions = report.includes('â„šī¸ Documented but not in CLI'); + + if (hasMissingOptions || hasExtraOptions) { + // Create issue + await github.rest.issues.create({ + owner: context.repo.owner, + repo: context.repo.repo, + title: `CLI Documentation Updates Needed - ${product} v${version}`, + body: `## CLI Documentation Audit Results + +The following documentation issues were found during the release of **${product} v${version}**: + +${report} + +### Action Items: +- [ ] Review and update documentation for commands with missing options +- [ ] Remove documentation for deprecated options +- [ ] Verify all examples work with the new version +- [ ] Update any version-specific content + +--- +*This issue was automatically generated during the release process.*`, + labels: ['documentation', 'cli-audit', 'release', product], + milestone: version // Assumes milestone exists for version + }); + + console.log('Created issue for CLI documentation updates'); + } else { + console.log('No documentation issues found - skipping issue creation'); + } + + summary: + name: Release Summary + needs: [generate-release-notes, audit-cli-documentation, create-documentation-pr, create-audit-issue] + runs-on: ubuntu-latest + if: always() + + steps: + - name: Generate summary + run: | + echo "# Release Documentation Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "## Release Information" >> $GITHUB_STEP_SUMMARY + echo "- **Product**: ${{ github.event.inputs.product }}" >> $GITHUB_STEP_SUMMARY + echo "- **Version**: ${{ github.event.inputs.version }}" >> $GITHUB_STEP_SUMMARY + echo "- **Previous Version**: ${{ github.event.inputs.previous_version }}" >> $GITHUB_STEP_SUMMARY + echo "- **Dry Run**: ${{ github.event.inputs.dry_run }}" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + echo "## Workflow Results" >> $GITHUB_STEP_SUMMARY + echo "| Step | Status |" >> $GITHUB_STEP_SUMMARY + echo "|------|--------|" >> $GITHUB_STEP_SUMMARY + echo "| Generate Release Notes | ${{ needs.generate-release-notes.result }} |" >> $GITHUB_STEP_SUMMARY + echo "| CLI Documentation Audit | ${{ needs.audit-cli-documentation.result }} |" >> $GITHUB_STEP_SUMMARY + echo "| Create Documentation PR | ${{ needs.create-documentation-pr.result }} |" >> $GITHUB_STEP_SUMMARY + echo "| Create Audit Issue | ${{ needs.create-audit-issue.result }} |" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + if [ "${{ github.event.inputs.dry_run }}" == "true" ]; then + echo "**Note**: This was a dry run. No PRs or issues were created." >> $GITHUB_STEP_SUMMARY + fi \ No newline at end of file diff --git a/.github/workflows/trigger-on-release.yml b/.github/workflows/trigger-on-release.yml new file mode 100644 index 000000000..cbf4419c2 --- /dev/null +++ b/.github/workflows/trigger-on-release.yml @@ -0,0 +1,61 @@ +name: Trigger Documentation Update on Release + +on: + # Can be triggered by external workflows using repository_dispatch + repository_dispatch: + types: [influxdb3-release] + + # Can also be triggered via GitHub API + # Example: + # curl -X POST \ + # -H "Authorization: token $GITHUB_TOKEN" \ + # -H "Accept: application/vnd.github.v3+json" \ + # https://api.github.com/repos/influxdata/docs-v2/dispatches \ + # -d '{"event_type":"influxdb3-release","client_payload":{"product":"core","version":"3.0.0","previous_version":"2.9.0"}}' + +jobs: + trigger-release-workflow: + name: Trigger Release Documentation + runs-on: ubuntu-latest + + steps: + - name: Validate payload + run: | + if [ -z "${{ github.event.client_payload.product }}" ]; then + echo "Error: product is required in client_payload" + exit 1 + fi + + if [ -z "${{ github.event.client_payload.version }}" ]; then + echo "Error: version is required in client_payload" + exit 1 + fi + + if [ -z "${{ github.event.client_payload.previous_version }}" ]; then + echo "Error: previous_version is required in client_payload" + exit 1 + fi + + echo "Received release notification:" + echo "Product: ${{ github.event.client_payload.product }}" + echo "Version: ${{ github.event.client_payload.version }}" + echo "Previous Version: ${{ github.event.client_payload.previous_version }}" + + - name: Trigger release documentation workflow + uses: actions/github-script@v7 + with: + script: | + await github.rest.actions.createWorkflowDispatch({ + owner: context.repo.owner, + repo: context.repo.repo, + workflow_id: 'influxdb3-release.yml', + ref: 'master', + inputs: { + product: '${{ github.event.client_payload.product }}', + version: '${{ github.event.client_payload.version }}', + previous_version: '${{ github.event.client_payload.previous_version }}', + dry_run: '${{ github.event.client_payload.dry_run || 'false' }}' + } + }); + + console.log('Successfully triggered release documentation workflow'); \ No newline at end of file diff --git a/helper-scripts/influxdb3-monolith/apply-cli-patches.js b/helper-scripts/influxdb3-monolith/apply-cli-patches.js new file mode 100755 index 000000000..07c2f7d71 --- /dev/null +++ b/helper-scripts/influxdb3-monolith/apply-cli-patches.js @@ -0,0 +1,277 @@ +#!/usr/bin/env node + +/** + * Apply CLI documentation patches generated by audit-cli-documentation.js + * Usage: node apply-cli-patches.js [core|enterprise|both] [--dry-run] + */ + +import { promises as fs } from 'fs'; +import { join, dirname } from 'path'; +import { fileURLToPath } from 'url'; +import { process } from 'node:process'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +// Color codes +const Colors = { + RED: '\x1b[0;31m', + GREEN: '\x1b[0;32m', + YELLOW: '\x1b[1;33m', + BLUE: '\x1b[0;34m', + NC: '\x1b[0m', // No Color +}; + +async function fileExists(path) { + try { + await fs.access(path); + return true; + } catch { + return false; + } +} + +async function ensureDir(dir) { + await fs.mkdir(dir, { recursive: true }); +} + +async function extractFrontmatter(content) { + const lines = content.split('\n'); + if (lines[0] !== '---') return { frontmatter: null, content }; + + const frontmatterLines = []; + let i = 1; + while (i < lines.length && lines[i] !== '---') { + frontmatterLines.push(lines[i]); + i++; + } + + if (i >= lines.length) return { frontmatter: null, content }; + + const frontmatterText = frontmatterLines.join('\n'); + const remainingContent = lines.slice(i + 1).join('\n'); + + return { frontmatter: frontmatterText, content: remainingContent }; +} + +async function getActualDocumentationPath(docPath, projectRoot) { + // Check if the documentation file exists and has a source field + const fullPath = join(projectRoot, docPath); + + if (await fileExists(fullPath)) { + const content = await fs.readFile(fullPath, 'utf8'); + const { frontmatter } = await extractFrontmatter(content); + + if (frontmatter) { + // Look for source: field in frontmatter + const sourceMatch = frontmatter.match(/^source:\s*(.+)$/m); + if (sourceMatch) { + const sourcePath = sourceMatch[1].trim(); + return sourcePath; + } + } + } + + return docPath; +} + +async function applyPatches(product, dryRun = false) { + const patchDir = join( + dirname(__dirname), + 'output', + 'cli-audit', + 'patches', + product + ); + const projectRoot = join(__dirname, '..', '..'); + + console.log( + `${Colors.BLUE}📋 Applying CLI documentation patches for ${product}${Colors.NC}` + ); + if (dryRun) { + console.log( + `${Colors.YELLOW}🔍 DRY RUN - No files will be created${Colors.NC}` + ); + } + console.log(); + + // Check if patch directory exists + if (!(await fileExists(patchDir))) { + console.log(`${Colors.YELLOW}No patches found for ${product}.${Colors.NC}`); + console.log("Run 'yarn audit:cli' first to generate patches."); + return; + } + + // Read all patch files + const patchFiles = await fs.readdir(patchDir); + const mdFiles = patchFiles.filter((f) => f.endsWith('.md')); + + if (mdFiles.length === 0) { + console.log( + `${Colors.YELLOW}No patch files found in ${patchDir}${Colors.NC}` + ); + return; + } + + console.log(`Found ${mdFiles.length} patch file(s) to apply:\n`); + + // Map patch files to their destination + const baseCliPath = `content/influxdb3/${product}/reference/cli/influxdb3`; + const commandToFile = { + 'create-database.md': `${baseCliPath}/create/database.md`, + 'create-token.md': `${baseCliPath}/create/token/_index.md`, + 'create-token-admin.md': `${baseCliPath}/create/token/admin.md`, + 'create-trigger.md': `${baseCliPath}/create/trigger.md`, + 'create-table.md': `${baseCliPath}/create/table.md`, + 'create-last_cache.md': `${baseCliPath}/create/last_cache.md`, + 'create-distinct_cache.md': `${baseCliPath}/create/distinct_cache.md`, + 'show-databases.md': `${baseCliPath}/show/databases.md`, + 'show-tokens.md': `${baseCliPath}/show/tokens.md`, + 'delete-database.md': `${baseCliPath}/delete/database.md`, + 'delete-table.md': `${baseCliPath}/delete/table.md`, + 'query.md': `${baseCliPath}/query.md`, + 'write.md': `${baseCliPath}/write.md`, + }; + + let applied = 0; + let skipped = 0; + + for (const patchFile of mdFiles) { + const destinationPath = commandToFile[patchFile]; + + if (!destinationPath) { + console.log( + `${Colors.YELLOW}âš ī¸ Unknown patch file: ${patchFile}${Colors.NC}` + ); + continue; + } + + // Get the actual documentation path (handles source: frontmatter) + const actualPath = await getActualDocumentationPath( + destinationPath, + projectRoot + ); + const fullDestPath = join(projectRoot, actualPath); + const patchPath = join(patchDir, patchFile); + + // Check if destination already exists + if (await fileExists(fullDestPath)) { + console.log( + `${Colors.YELLOW}â­ī¸ Skipping${Colors.NC} ${patchFile} - destination already exists:` + ); + console.log(` ${actualPath}`); + skipped++; + continue; + } + + if (dryRun) { + console.log(`${Colors.BLUE}🔍 Would create${Colors.NC} ${actualPath}`); + console.log(` from patch: ${patchFile}`); + if (actualPath !== destinationPath) { + console.log(` (resolved from: ${destinationPath})`); + } + applied++; + } else { + try { + // Ensure destination directory exists + await ensureDir(dirname(fullDestPath)); + + // Copy patch to destination + const content = await fs.readFile(patchPath, 'utf8'); + + // Update the menu configuration based on product + let updatedContent = content; + if (product === 'enterprise') { + updatedContent = content + .replace('influxdb3/core/tags:', 'influxdb3/enterprise/tags:') + .replace( + 'influxdb3_core_reference:', + 'influxdb3_enterprise_reference:' + ); + } + + await fs.writeFile(fullDestPath, updatedContent); + + console.log(`${Colors.GREEN}✅ Created${Colors.NC} ${actualPath}`); + console.log(` from patch: ${patchFile}`); + if (actualPath !== destinationPath) { + console.log(` (resolved from: ${destinationPath})`); + } + applied++; + } catch (error) { + console.log( + `${Colors.RED}❌ Error${Colors.NC} creating ${actualPath}:` + ); + console.log(` ${error.message}`); + } + } + } + + console.log(); + console.log(`${Colors.BLUE}Summary:${Colors.NC}`); + console.log(`- Patches ${dryRun ? 'would be' : ''} applied: ${applied}`); + console.log(`- Files skipped (already exist): ${skipped}`); + console.log(`- Total patch files: ${mdFiles.length}`); + + if (!dryRun && applied > 0) { + console.log(); + console.log( + `${Colors.GREEN}✨ Success!${Colors.NC} Created ${applied} new ` + + 'documentation file(s).' + ); + console.log(); + console.log('Next steps:'); + console.log('1. Review the generated files and customize the content'); + console.log('2. Add proper examples with placeholders'); + console.log('3. Update descriptions and add any missing options'); + console.log('4. Run tests: yarn test:links'); + } +} + +async function main() { + const args = process.argv.slice(2); + const product = + args.find((arg) => ['core', 'enterprise', 'both'].includes(arg)) || 'both'; + const dryRun = args.includes('--dry-run'); + + if (args.includes('--help') || args.includes('-h')) { + console.log( + 'Usage: node apply-cli-patches.js [core|enterprise|both] [--dry-run]' + ); + console.log(); + console.log('Options:'); + console.log( + ' --dry-run Show what would be done without creating files' + ); + console.log(); + console.log('Examples:'); + console.log( + ' node apply-cli-patches.js # Apply patches for both products' + ); + console.log( + ' node apply-cli-patches.js core --dry-run # Preview core patches' + ); + console.log( + ' node apply-cli-patches.js enterprise # Apply enterprise patches' + ); + process.exit(0); + } + + try { + if (product === 'both') { + await applyPatches('core', dryRun); + console.log(); + await applyPatches('enterprise', dryRun); + } else { + await applyPatches(product, dryRun); + } + } catch (error) { + console.error(`${Colors.RED}Error:${Colors.NC}`, error.message); + process.exit(1); + } +} + +// Run if called directly +if (import.meta.url === `file://${process.argv[1]}`) { + main(); +} diff --git a/helper-scripts/influxdb3-monolith/audit-cli-documentation.js b/helper-scripts/influxdb3-monolith/audit-cli-documentation.js new file mode 100755 index 000000000..d51489f5a --- /dev/null +++ b/helper-scripts/influxdb3-monolith/audit-cli-documentation.js @@ -0,0 +1,960 @@ +#!/usr/bin/env node + +/** + * Audit CLI documentation against current CLI help output + * Usage: node audit-cli-documentation.js [core|enterprise|both] [version] + * Example: node audit-cli-documentation.js core 3.2.0 + */ + +import { spawn } from 'child_process'; +import { promises as fs } from 'fs'; +import { homedir } from 'os'; +import { join, dirname } from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +// Color codes +const Colors = { + RED: '\x1b[0;31m', + GREEN: '\x1b[0;32m', + YELLOW: '\x1b[1;33m', + BLUE: '\x1b[0;34m', + NC: '\x1b[0m', // No Color +}; + +class CLIDocAuditor { + constructor(product = 'both', version = 'local') { + this.product = product; + this.version = version; + this.outputDir = join(dirname(__dirname), 'output', 'cli-audit'); + + // Token paths - check environment variables first (Docker Compose), then fall back to local files + const coreTokenEnv = process.env.INFLUXDB3_CORE_TOKEN; + const enterpriseTokenEnv = process.env.INFLUXDB3_ENTERPRISE_TOKEN; + + if (coreTokenEnv && this.fileExists(coreTokenEnv)) { + // Running in Docker Compose with secrets + this.coreTokenFile = coreTokenEnv; + this.enterpriseTokenFile = enterpriseTokenEnv; + } else { + // Running locally + this.coreTokenFile = join(homedir(), '.env.influxdb3-core-admin-token'); + this.enterpriseTokenFile = join( + homedir(), + '.env.influxdb3-enterprise-admin-token' + ); + } + + // Commands to extract help for + this.mainCommands = [ + 'create', + 'delete', + 'disable', + 'enable', + 'query', + 'show', + 'test', + 'update', + 'write', + ]; + this.subcommands = [ + 'create database', + 'create token admin', + 'create token', + 'create trigger', + 'create last_cache', + 'create distinct_cache', + 'create table', + 'show databases', + 'show tokens', + 'show system', + 'delete database', + 'delete table', + 'delete trigger', + 'update database', + 'test wal_plugin', + 'test schedule_plugin', + ]; + + // Map for command tracking during option parsing + this.commandOptionsMap = {}; + } + + async fileExists(path) { + try { + await fs.access(path); + return true; + } catch { + return false; + } + } + + async ensureDir(dir) { + await fs.mkdir(dir, { recursive: true }); + } + + async loadTokens() { + let coreToken = null; + let enterpriseToken = null; + + try { + if (await this.fileExists(this.coreTokenFile)) { + const stat = await fs.stat(this.coreTokenFile); + if (stat.size > 0) { + coreToken = (await fs.readFile(this.coreTokenFile, 'utf8')).trim(); + } + } + } catch (e) { + // Token file doesn't exist or can't be read + } + + try { + if (await this.fileExists(this.enterpriseTokenFile)) { + const stat = await fs.stat(this.enterpriseTokenFile); + if (stat.size > 0) { + enterpriseToken = ( + await fs.readFile(this.enterpriseTokenFile, 'utf8') + ).trim(); + } + } + } catch (e) { + // Token file doesn't exist or can't be read + } + + return { coreToken, enterpriseToken }; + } + + runCommand(cmd, args = []) { + return new Promise((resolve) => { + const child = spawn(cmd, args, { encoding: 'utf8' }); + let stdout = ''; + let stderr = ''; + + child.stdout.on('data', (data) => { + stdout += data.toString(); + }); + + child.stderr.on('data', (data) => { + stderr += data.toString(); + }); + + child.on('close', (code) => { + resolve({ code, stdout, stderr }); + }); + + child.on('error', (err) => { + resolve({ code: 1, stdout: '', stderr: err.message }); + }); + }); + } + + async extractCurrentCLI(product, outputFile) { + process.stdout.write( + `Extracting current CLI help from influxdb3-${product}...` + ); + + await this.loadTokens(); + + if (this.version === 'local') { + const containerName = `influxdb3-${product}`; + + // Check if container is running + const { code, stdout } = await this.runCommand('docker', [ + 'ps', + '--format', + '{{.Names}}', + ]); + if (code !== 0 || !stdout.includes(containerName)) { + console.log(` ${Colors.RED}✗${Colors.NC}`); + console.log(`Error: Container ${containerName} is not running.`); + console.log(`Start it with: docker compose up -d influxdb3-${product}`); + return false; + } + + // Extract comprehensive help + let fileContent = ''; + + // Main help + const mainHelp = await this.runCommand('docker', [ + 'exec', + containerName, + 'influxdb3', + '--help', + ]); + fileContent += mainHelp.code === 0 ? mainHelp.stdout : mainHelp.stderr; + + // Extract all subcommand help + for (const cmd of this.mainCommands) { + fileContent += `\n\n===== influxdb3 ${cmd} --help =====\n`; + const cmdHelp = await this.runCommand('docker', [ + 'exec', + containerName, + 'influxdb3', + cmd, + '--help', + ]); + fileContent += cmdHelp.code === 0 ? cmdHelp.stdout : cmdHelp.stderr; + } + + // Extract detailed subcommand help + for (const subcmd of this.subcommands) { + fileContent += `\n\n===== influxdb3 ${subcmd} --help =====\n`; + const cmdParts = [ + 'exec', + containerName, + 'influxdb3', + ...subcmd.split(' '), + '--help', + ]; + const subcmdHelp = await this.runCommand('docker', cmdParts); + fileContent += + subcmdHelp.code === 0 ? subcmdHelp.stdout : subcmdHelp.stderr; + } + + await fs.writeFile(outputFile, fileContent); + console.log(` ${Colors.GREEN}✓${Colors.NC}`); + } else { + // Use specific version image + const image = `influxdb:${this.version}-${product}`; + + process.stdout.write(`Extracting CLI help from ${image}...`); + + // Pull image if needed + const pullResult = await this.runCommand('docker', ['pull', image]); + if (pullResult.code !== 0) { + console.log(` ${Colors.RED}✗${Colors.NC}`); + console.log(`Error: Failed to pull image ${image}`); + return false; + } + + // Extract help from specific version + let fileContent = ''; + + // Main help + const mainHelp = await this.runCommand('docker', [ + 'run', + '--rm', + image, + 'influxdb3', + '--help', + ]); + fileContent += mainHelp.code === 0 ? mainHelp.stdout : mainHelp.stderr; + + // Extract subcommand help + for (const cmd of this.mainCommands) { + fileContent += `\n\n===== influxdb3 ${cmd} --help =====\n`; + const cmdHelp = await this.runCommand('docker', [ + 'run', + '--rm', + image, + 'influxdb3', + cmd, + '--help', + ]); + fileContent += cmdHelp.code === 0 ? cmdHelp.stdout : cmdHelp.stderr; + } + + await fs.writeFile(outputFile, fileContent); + console.log(` ${Colors.GREEN}✓${Colors.NC}`); + } + + return true; + } + + async parseCLIHelp(helpFile, parsedFile) { + const content = await fs.readFile(helpFile, 'utf8'); + const lines = content.split('\n'); + + let output = '# CLI Commands and Options\n\n'; + let currentCommand = ''; + let inOptions = false; + + for (const line of lines) { + // Detect command headers + if (line.startsWith('===== influxdb3') && line.endsWith('--help =====')) { + currentCommand = line + .replace('===== ', '') + .replace(' --help =====', '') + .trim(); + output += `## ${currentCommand}\n\n`; + inOptions = false; + // Initialize options list for this command + this.commandOptionsMap[currentCommand] = []; + } + // Detect options sections + else if (line.trim() === 'Options:') { + output += '### Options:\n\n'; + inOptions = true; + } + // Parse option lines + else if (inOptions && /^\s*-/.test(line)) { + // Extract option and description + const optionMatch = line.match(/--[a-z][a-z0-9-]*/); + const shortMatch = line.match(/\s-[a-zA-Z],/); + + if (optionMatch) { + const option = optionMatch[0]; + const shortOption = shortMatch + ? shortMatch[0].replace(/[,\s]/g, '') + : null; + + // Extract description by removing option parts + let description = line.replace(/^\s*-[^\s]*\s*/, ''); + description = description.replace(/^\s*--[^\s]*\s*/, '').trim(); + + if (shortOption) { + output += `- \`${shortOption}, ${option}\`: ${description}\n`; + } else { + output += `- \`${option}\`: ${description}\n`; + } + + // Store option with its command context + if (currentCommand && option) { + this.commandOptionsMap[currentCommand].push(option); + } + } + } + // Reset options flag for new sections + else if (/^[A-Z][a-z]+:$/.test(line.trim())) { + inOptions = false; + } + } + + await fs.writeFile(parsedFile, output); + } + + findDocsPath(product) { + if (product === 'core') { + return 'content/influxdb3/core/reference/cli/influxdb3'; + } else if (product === 'enterprise') { + return 'content/influxdb3/enterprise/reference/cli/influxdb3'; + } + return ''; + } + + async extractCommandHelp(content, command) { + // Find the section for this specific command in the CLI help + const lines = content.split('\n'); + let inCommand = false; + let helpText = []; + const commandHeader = `===== influxdb3 ${command} --help =====`; + + for (let i = 0; i < lines.length; i++) { + if (lines[i] === commandHeader) { + inCommand = true; + continue; + } + if (inCommand && lines[i].startsWith('===== influxdb3')) { + break; + } + if (inCommand) { + helpText.push(lines[i]); + } + } + + return helpText.join('\n').trim(); + } + + async generateDocumentationTemplate(command, helpText) { + // Parse the help text to extract description and options + const lines = helpText.split('\n'); + let description = ''; + let usage = ''; + let options = []; + let inOptions = false; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + + if (i === 0 && !line.startsWith('Usage:') && line.trim()) { + description = line.trim(); + } + if (line.startsWith('Usage:')) { + usage = line.replace('Usage:', '').trim(); + } + if (line.trim() === 'Options:') { + inOptions = true; + continue; + } + if (inOptions && /^\s*-/.test(line)) { + const optionMatch = line.match(/--([a-z][a-z0-9-]*)/); + const shortMatch = line.match(/\s-([a-zA-Z]),/); + if (optionMatch) { + const optionName = optionMatch[1]; + const shortOption = shortMatch ? shortMatch[1] : null; + let optionDesc = line + .replace(/^\s*-[^\s]*\s*/, '') + .replace(/^\s*--[^\s]*\s*/, '') + .trim(); + + options.push({ + name: optionName, + short: shortOption, + description: optionDesc, + }); + } + } + } + + // Generate markdown template + let template = `--- +title: influxdb3 ${command} +description: > + The \`influxdb3 ${command}\` command ${description.toLowerCase()}. +influxdb3/core/tags: [cli] +menu: + influxdb3_core_reference: + parent: influxdb3 cli +weight: 201 +--- + +# influxdb3 ${command} + +${description} + +## Usage + +\`\`\`bash +${usage || `influxdb3 ${command} [OPTIONS]`} +\`\`\` + +`; + + if (options.length > 0) { + template += `## Options + +| Option | Description | +|--------|-------------| +`; + + for (const opt of options) { + const optionDisplay = opt.short + ? `\`-${opt.short}\`, \`--${opt.name}\`` + : `\`--${opt.name}\``; + template += `| ${optionDisplay} | ${opt.description} |\n`; + } + } + + template += ` +## Examples + +### Example 1: Basic usage + +{{% code-placeholders "PLACEHOLDER1|PLACEHOLDER2" %}} +\`\`\`bash +influxdb3 ${command} --example PLACEHOLDER1 +\`\`\` +{{% /code-placeholders %}} + +Replace the following: + +- {{% code-placeholder-key %}}\`PLACEHOLDER1\`{{% /code-placeholder-key %}}: Description of placeholder +`; + + return template; + } + + async extractFrontmatter(content) { + const lines = content.split('\n'); + if (lines[0] !== '---') return { frontmatter: null, content }; + + const frontmatterLines = []; + let i = 1; + while (i < lines.length && lines[i] !== '---') { + frontmatterLines.push(lines[i]); + i++; + } + + if (i >= lines.length) return { frontmatter: null, content }; + + const frontmatterText = frontmatterLines.join('\n'); + const remainingContent = lines.slice(i + 1).join('\n'); + + return { frontmatter: frontmatterText, content: remainingContent }; + } + + async getActualContentPath(filePath) { + // Get the actual content path, resolving source fields + try { + const content = await fs.readFile(filePath, 'utf8'); + const { frontmatter } = await this.extractFrontmatter(content); + + if (frontmatter) { + const sourceMatch = frontmatter.match(/^source:\s*(.+)$/m); + if (sourceMatch) { + let sourcePath = sourceMatch[1].trim(); + // Handle relative paths from project root + if (sourcePath.startsWith('/shared/')) { + sourcePath = `content${sourcePath}`; + } + return sourcePath; + } + } + return null; // No source field found + } catch { + return null; + } + } + + async parseDocumentedOptions(filePath) { + // Parse a documentation file to extract all documented options + try { + const content = await fs.readFile(filePath, 'utf8'); + const options = []; + + // Look for options in various patterns: + // 1. Markdown tables with option columns + // 2. Option lists with backticks + // 3. Code examples with --option flags + + // Pattern 1: Markdown tables (| Option | Description |) + const tableMatches = content.match(/\|\s*`?--[a-z][a-z0-9-]*`?\s*\|/gi); + if (tableMatches) { + for (const match of tableMatches) { + const option = match.match(/--[a-z][a-z0-9-]*/i); + if (option) { + options.push(option[0]); + } + } + } + + // Pattern 2: Backtick-enclosed options in text + const backtickMatches = content.match(/`--[a-z][a-z0-9-]*`/gi); + if (backtickMatches) { + for (const match of backtickMatches) { + const option = match.replace(/`/g, ''); + options.push(option); + } + } + + // Pattern 3: Options in code blocks + const codeBlockMatches = content.match(/```[\s\S]*?```/g); + if (codeBlockMatches) { + for (const block of codeBlockMatches) { + const blockOptions = block.match(/--[a-z][a-z0-9-]*/gi); + if (blockOptions) { + options.push(...blockOptions); + } + } + } + + // Pattern 4: Environment variable mappings (INFLUXDB3_* to --option) + const envMatches = content.match( + /\|\s*`INFLUXDB3_[^`]*`\s*\|\s*`--[a-z][a-z0-9-]*`\s*\|/gi + ); + if (envMatches) { + for (const match of envMatches) { + const option = match.match(/--[a-z][a-z0-9-]*/); + if (option) { + options.push(option[0]); + } + } + } + + // Remove duplicates and return sorted + return [...new Set(options)].sort(); + } catch { + return []; + } + } + + async auditDocs(product, cliFile, auditFile) { + const docsPath = this.findDocsPath(product); + const sharedPath = 'content/shared/influxdb3-cli'; + const patchDir = join(this.outputDir, 'patches', product); + await this.ensureDir(patchDir); + + let output = `# CLI Documentation Audit - ${product}\n`; + output += `Generated: ${new Date().toISOString()}\n\n`; + + // GitHub base URL for edit links + const githubBase = 'https://github.com/influxdata/docs-v2/edit/master'; + const githubNewBase = 'https://github.com/influxdata/docs-v2/new/master'; + + // VSCode links for local editing + const vscodeBase = 'vscode://file'; + const projectRoot = join(__dirname, '..', '..'); + + // Check for missing documentation + output += '## Missing Documentation\n\n'; + + let missingCount = 0; + const missingDocs = []; + + // Map commands to expected documentation files + const commandToFile = { + 'create database': 'create/database.md', + 'create token': 'create/token/_index.md', + 'create token admin': 'create/token/admin.md', + 'create trigger': 'create/trigger.md', + 'create table': 'create/table.md', + 'create last_cache': 'create/last_cache.md', + 'create distinct_cache': 'create/distinct_cache.md', + 'show databases': 'show/databases.md', + 'show tokens': 'show/tokens.md', + 'delete database': 'delete/database.md', + 'delete table': 'delete/table.md', + query: 'query.md', + write: 'write.md', + }; + + // Extract commands from CLI help + const content = await fs.readFile(cliFile, 'utf8'); + const lines = content.split('\n'); + + for (const line of lines) { + if (line.startsWith('===== influxdb3') && line.endsWith('--help =====')) { + const command = line + .replace('===== influxdb3 ', '') + .replace(' --help =====', ''); + + if (commandToFile[command]) { + const expectedFile = commandToFile[command]; + const productFile = join(docsPath, expectedFile); + const sharedFile = join(sharedPath, expectedFile); + + const productExists = await this.fileExists(productFile); + const sharedExists = await this.fileExists(sharedFile); + + let needsContent = false; + let targetPath = null; + let stubPath = null; + + if (!productExists && !sharedExists) { + // Completely missing + needsContent = true; + targetPath = productFile; + } else if (productExists) { + // Check if it has a source field pointing to missing content + const actualPath = await this.getActualContentPath(productFile); + if (actualPath && !(await this.fileExists(actualPath))) { + needsContent = true; + targetPath = actualPath; + stubPath = productFile; + } + } else if (sharedExists) { + // Shared file exists, check if it has content + const actualPath = await this.getActualContentPath(sharedFile); + if (actualPath && !(await this.fileExists(actualPath))) { + needsContent = true; + targetPath = actualPath; + stubPath = sharedFile; + } + } + + if (needsContent && targetPath) { + const githubNewUrl = `${githubNewBase}/${targetPath}`; + const localPath = join(projectRoot, targetPath); + + output += `- **Missing**: Documentation for \`influxdb3 ${command}\`\n`; + if (stubPath) { + output += ` - Stub exists at: \`${stubPath}\`\n`; + output += ` - Content needed at: \`${targetPath}\`\n`; + } else { + output += ` - Expected: \`${targetPath}\` or \`${sharedFile}\`\n`; + } + output += ` - [Create on GitHub](${githubNewUrl})\n`; + output += ` - Local: \`${localPath}\`\n`; + + // Generate documentation template + const helpText = await this.extractCommandHelp(content, command); + const docTemplate = await this.generateDocumentationTemplate( + command, + helpText + ); + + // Save patch file + const patchFileName = `${command.replace(/ /g, '-')}.md`; + const patchFile = join(patchDir, patchFileName); + await fs.writeFile(patchFile, docTemplate); + + output += ` - **Template generated**: \`${patchFile}\`\n`; + + missingDocs.push({ command, file: targetPath, patchFile }); + missingCount++; + } + } + } + } + + if (missingCount === 0) { + output += 'No missing documentation files detected.\n'; + } else { + output += `\n### Quick Actions\n\n`; + output += `Copy and paste these commands to create missing documentation:\n\n`; + output += `\`\`\`bash\n`; + for (const doc of missingDocs) { + const relativePatch = join( + 'helper-scripts/output/cli-audit/patches', + product, + `${doc.command.replace(/ /g, '-')}.md` + ); + output += `# Create ${doc.command} documentation\n`; + output += `mkdir -p $(dirname ${doc.file})\n`; + output += `cp ${relativePatch} ${doc.file}\n\n`; + } + output += `\`\`\`\n`; + } + + output += '\n'; + + // Check for outdated options in existing docs + output += '## Existing Documentation Review\n\n'; + + // Parse CLI help first to populate commandOptionsMap + const parsedFile = join( + this.outputDir, + `parsed-cli-${product}-${this.version}.md` + ); + await this.parseCLIHelp(cliFile, parsedFile); + + // For each command, check if documentation exists and compare content + const existingDocs = []; + for (const [command, expectedFile] of Object.entries(commandToFile)) { + const productFile = join(docsPath, expectedFile); + const sharedFile = join(sharedPath, expectedFile); + + let docFile = null; + let actualContentFile = null; + + // Find the documentation file + if (await this.fileExists(productFile)) { + docFile = productFile; + // Check if it's a stub with source field + const actualPath = await this.getActualContentPath(productFile); + actualContentFile = actualPath + ? join(projectRoot, actualPath) + : join(projectRoot, productFile); + } else if (await this.fileExists(sharedFile)) { + docFile = sharedFile; + actualContentFile = join(projectRoot, sharedFile); + } + + if (docFile && (await this.fileExists(actualContentFile))) { + const githubEditUrl = `${githubBase}/${docFile}`; + const localPath = join(projectRoot, docFile); + const vscodeUrl = `${vscodeBase}/${localPath}`; + + // Get CLI options for this command + const cliOptions = this.commandOptionsMap[`influxdb3 ${command}`] || []; + + // Parse documentation content to find documented options + const documentedOptions = + await this.parseDocumentedOptions(actualContentFile); + + // Find missing options (in CLI but not in docs) + const missingOptions = cliOptions.filter( + (opt) => !documentedOptions.includes(opt) + ); + + // Find extra options (in docs but not in CLI) + const extraOptions = documentedOptions.filter( + (opt) => !cliOptions.includes(opt) + ); + + existingDocs.push({ + command, + file: docFile, + actualContentFile: actualContentFile.replace( + join(projectRoot, ''), + '' + ), + githubUrl: githubEditUrl, + localPath, + vscodeUrl, + cliOptions, + documentedOptions, + missingOptions, + extraOptions, + }); + } + } + + if (existingDocs.length > 0) { + output += 'Review these existing documentation files for accuracy:\n\n'; + + for (const doc of existingDocs) { + output += `### \`influxdb3 ${doc.command}\`\n`; + output += `- **File**: \`${doc.file}\`\n`; + if (doc.actualContentFile !== doc.file) { + output += `- **Content**: \`${doc.actualContentFile}\`\n`; + } + output += `- [Edit on GitHub](${doc.githubUrl})\n`; + output += `- [Open in VS Code](${doc.vscodeUrl})\n`; + output += `- **Local**: \`${doc.localPath}\`\n`; + + // Show option analysis + if (doc.missingOptions.length > 0) { + output += `- **âš ī¸ Missing from docs** (${doc.missingOptions.length} options):\n`; + for (const option of doc.missingOptions.sort()) { + output += ` - \`${option}\`\n`; + } + } + + if (doc.extraOptions.length > 0) { + output += `- **â„šī¸ Documented but not in CLI** (${doc.extraOptions.length} options):\n`; + for (const option of doc.extraOptions.sort()) { + output += ` - \`${option}\`\n`; + } + } + + if (doc.missingOptions.length === 0 && doc.extraOptions.length === 0) { + output += `- **✅ Options match** (${doc.cliOptions.length} options)\n`; + } + + if (doc.cliOptions.length > 0) { + output += `- **All CLI Options** (${doc.cliOptions.length}):\n`; + const uniqueOptions = [...new Set(doc.cliOptions)].sort(); + for (const option of uniqueOptions) { + const status = doc.missingOptions.includes(option) ? '❌' : '✅'; + output += ` - ${status} \`${option}\`\n`; + } + } + output += '\n'; + } + } + + output += '\n## Summary\n'; + output += `- Missing documentation files: ${missingCount}\n`; + output += `- Existing documentation files: ${existingDocs.length}\n`; + output += `- Generated templates: ${missingCount}\n`; + output += '- Options are grouped by command for easier review\n\n'; + + output += '## Automation Suggestions\n\n'; + output += + '1. **Use generated templates**: Check the `patches` directory for pre-filled documentation templates\n'; + output += + '2. **Batch creation**: Use the shell commands above to quickly create all missing files\n'; + output += + '3. **CI Integration**: Add this audit to your CI pipeline to catch missing docs early\n'; + output += + '4. **Auto-PR**: Create a GitHub Action that runs this audit and opens PRs for missing docs\n\n'; + + await fs.writeFile(auditFile, output); + console.log(`📄 Audit complete: ${auditFile}`); + + if (missingCount > 0) { + console.log( + `📝 Generated ${missingCount} documentation templates in: ${patchDir}` + ); + } + } + + async run() { + console.log( + `${Colors.BLUE}🔍 InfluxDB 3 CLI Documentation Audit${Colors.NC}` + ); + console.log('======================================='); + console.log(`Product: ${this.product}`); + console.log(`Version: ${this.version}`); + console.log(); + + // Ensure output directory exists + await this.ensureDir(this.outputDir); + + if (this.product === 'core') { + const cliFile = join( + this.outputDir, + `current-cli-core-${this.version}.txt` + ); + const auditFile = join( + this.outputDir, + `documentation-audit-core-${this.version}.md` + ); + + if (await this.extractCurrentCLI('core', cliFile)) { + await this.auditDocs('core', cliFile, auditFile); + } + } else if (this.product === 'enterprise') { + const cliFile = join( + this.outputDir, + `current-cli-enterprise-${this.version}.txt` + ); + const auditFile = join( + this.outputDir, + `documentation-audit-enterprise-${this.version}.md` + ); + + if (await this.extractCurrentCLI('enterprise', cliFile)) { + await this.auditDocs('enterprise', cliFile, auditFile); + } + } else if (this.product === 'both') { + // Core + const cliFileCore = join( + this.outputDir, + `current-cli-core-${this.version}.txt` + ); + const auditFileCore = join( + this.outputDir, + `documentation-audit-core-${this.version}.md` + ); + + if (await this.extractCurrentCLI('core', cliFileCore)) { + await this.auditDocs('core', cliFileCore, auditFileCore); + } + + // Enterprise + const cliFileEnt = join( + this.outputDir, + `current-cli-enterprise-${this.version}.txt` + ); + const auditFileEnt = join( + this.outputDir, + `documentation-audit-enterprise-${this.version}.md` + ); + + if (await this.extractCurrentCLI('enterprise', cliFileEnt)) { + await this.auditDocs('enterprise', cliFileEnt, auditFileEnt); + } + } else { + console.error(`Error: Invalid product '${this.product}'`); + console.error( + 'Usage: node audit-cli-documentation.js [core|enterprise|both] [version]' + ); + process.exit(1); + } + + console.log(); + console.log( + `${Colors.GREEN}✅ CLI documentation audit complete!${Colors.NC}` + ); + console.log(); + console.log('Next steps:'); + console.log(`1. Review the audit reports in: ${this.outputDir}`); + console.log('2. Update missing documentation files'); + console.log('3. Verify options match current CLI behavior'); + console.log('4. Update examples and usage patterns'); + } +} + +// Main execution +async function main() { + const args = process.argv.slice(2); + const product = args[0] || 'both'; + const version = args[1] || 'local'; + + // Validate product + if (!['core', 'enterprise', 'both'].includes(product)) { + console.error(`Error: Invalid product '${product}'`); + console.error( + 'Usage: node audit-cli-documentation.js [core|enterprise|both] [version]' + ); + console.error('Example: node audit-cli-documentation.js core 3.2.0'); + process.exit(1); + } + + const auditor = new CLIDocAuditor(product, version); + await auditor.run(); +} + +// Run if called directly +if (import.meta.url === `file://${process.argv[1]}`) { + main().catch((err) => { + console.error('Error:', err); + process.exit(1); + }); +} + +export { CLIDocAuditor }; diff --git a/helper-scripts/influxdb3-monolith/audit-cli-documentation.sh b/helper-scripts/influxdb3-monolith/audit-cli-documentation.sh deleted file mode 100755 index 7e3aea2b9..000000000 --- a/helper-scripts/influxdb3-monolith/audit-cli-documentation.sh +++ /dev/null @@ -1,316 +0,0 @@ -#!/bin/bash -# Audit CLI documentation against current CLI help output -# Usage: ./audit-cli-documentation.sh [core|enterprise|both] [version] -# Example: ./audit-cli-documentation.sh core 3.2.0 - -set -e - -# Color codes -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -NC='\033[0m' - -# Parse arguments -PRODUCT=${1:-both} -VERSION=${2:-local} - -echo -e "${BLUE}🔍 InfluxDB 3 CLI Documentation Audit${NC}" -echo "=======================================" -echo "Product: $PRODUCT" -echo "Version: $VERSION" -echo "" - -# Set up output directory -OUTPUT_DIR="helper-scripts/output/cli-audit" -mkdir -p "$OUTPUT_DIR" - -# Load tokens from secret files -load_tokens() { - SECRET_CORE_FILE="$HOME/.env.influxdb3-core-admin-token" - SECRET_ENT_FILE="$HOME/.env.influxdb3-enterprise-admin-token" - - if [ -f "$SECRET_CORE_FILE" ] && [ -s "$SECRET_CORE_FILE" ]; then - INFLUXDB3_CORE_TOKEN=$(cat "$SECRET_CORE_FILE") - fi - if [ -f "$SECRET_ENT_FILE" ] && [ -s "$SECRET_ENT_FILE" ]; then - INFLUXDB3_ENTERPRISE_TOKEN=$(cat "$SECRET_ENT_FILE") - fi -} - -# Get current CLI help for a product -extract_current_cli() { - local product=$1 - local output_file=$2 - - load_tokens - - if [ "$VERSION" == "local" ]; then - local container_name="influxdb3-${product}" - - echo -n "Extracting current CLI help from ${container_name}..." - - # Check if container is running - if ! docker ps --format '{{.Names}}' | grep -q "^${container_name}$"; then - echo -e " ${RED}✗${NC}" - echo "Error: Container ${container_name} is not running." - echo "Start it with: docker compose up -d influxdb3-${product}" - return 1 - fi - - # Extract comprehensive help - docker exec "${container_name}" influxdb3 --help > "$output_file" 2>&1 - - # Extract all subcommand help - for cmd in create delete disable enable query show test update write; do - echo "" >> "$output_file" - echo "===== influxdb3 $cmd --help =====" >> "$output_file" - docker exec "${container_name}" influxdb3 $cmd --help >> "$output_file" 2>&1 || true - done - - # Extract detailed subcommand help - local subcommands=( - "create database" - "create token admin" - "create token" - "create trigger" - "create last_cache" - "create distinct_cache" - "create table" - "show databases" - "show tokens" - "show system" - "delete database" - "delete table" - "delete trigger" - "update database" - "test wal_plugin" - "test schedule_plugin" - ) - - for subcmd in "${subcommands[@]}"; do - echo "" >> "$output_file" - echo "===== influxdb3 $subcmd --help =====" >> "$output_file" - docker exec "${container_name}" influxdb3 $subcmd --help >> "$output_file" 2>&1 || true - done - - echo -e " ${GREEN}✓${NC}" - else - # Use specific version image - local image="influxdb:${VERSION}-${product}" - - echo -n "Extracting CLI help from ${image}..." - - if ! docker pull "${image}" > /dev/null 2>&1; then - echo -e " ${RED}✗${NC}" - echo "Error: Failed to pull image ${image}" - return 1 - fi - - # Extract help from specific version - docker run --rm "${image}" influxdb3 --help > "$output_file" 2>&1 - - # Extract subcommand help - for cmd in create delete disable enable query show test update write; do - echo "" >> "$output_file" - echo "===== influxdb3 $cmd --help =====" >> "$output_file" - docker run --rm "${image}" influxdb3 $cmd --help >> "$output_file" 2>&1 || true - done - - echo -e " ${GREEN}✓${NC}" - fi -} - -# Parse CLI help to extract structured information -parse_cli_help() { - local help_file=$1 - local parsed_file=$2 - - echo "# CLI Commands and Options" > "$parsed_file" - echo "" >> "$parsed_file" - - local current_command="" - local in_options=false - - while IFS= read -r line; do - # Detect command headers - if echo "$line" | grep -q "^===== influxdb3.*--help ====="; then - current_command=$(echo "$line" | sed 's/^===== //' | sed 's/ --help =====//') - echo "## $current_command" >> "$parsed_file" - echo "" >> "$parsed_file" - in_options=false - # Detect options sections - elif echo "$line" | grep -q "^Options:"; then - echo "### Options:" >> "$parsed_file" - echo "" >> "$parsed_file" - in_options=true - # Parse option lines - elif [ "$in_options" = true ] && echo "$line" | grep -qE "^\s*-"; then - # Extract option and description - option=$(echo "$line" | grep -oE '\-\-[a-z][a-z0-9-]*' | head -1) - short_option=$(echo "$line" | grep -oE '\s-[a-zA-Z],' | sed 's/[, ]//g') - description=$(echo "$line" | sed 's/^[[:space:]]*-[^[:space:]]*[[:space:]]*//' | sed 's/^[[:space:]]*--[^[:space:]]*[[:space:]]*//') - - if [ -n "$option" ]; then - if [ -n "$short_option" ]; then - echo "- \`$short_option, $option\`: $description" >> "$parsed_file" - else - echo "- \`$option\`: $description" >> "$parsed_file" - fi - fi - # Reset options flag for new sections - elif echo "$line" | grep -qE "^[A-Z][a-z]+:$"; then - in_options=false - fi - done < "$help_file" -} - -# Find documentation files for a product -find_docs() { - local product=$1 - - case "$product" in - "core") - echo "content/influxdb3/core/reference/cli/influxdb3" - ;; - "enterprise") - echo "content/influxdb3/enterprise/reference/cli/influxdb3" - ;; - esac -} - -# Audit documentation against CLI -audit_docs() { - local product=$1 - local cli_file=$2 - local audit_file=$3 - - local docs_path=$(find_docs "$product") - local shared_path="content/shared/influxdb3-cli" - - echo "# CLI Documentation Audit - $product" > "$audit_file" - echo "Generated: $(date)" >> "$audit_file" - echo "" >> "$audit_file" - - # Check for missing documentation - echo "## Missing Documentation" >> "$audit_file" - echo "" >> "$audit_file" - - local missing_count=0 - - # Extract commands from CLI help - grep "^===== influxdb3.*--help =====" "$cli_file" | while read -r line; do - local command=$(echo "$line" | sed 's/^===== influxdb3 //' | sed 's/ --help =====//') - local expected_file="" - - # Map command to expected documentation file - case "$command" in - "create database") expected_file="create/database.md" ;; - "create token") expected_file="create/token/_index.md" ;; - "create token admin") expected_file="create/token/admin.md" ;; - "create trigger") expected_file="create/trigger.md" ;; - "create table") expected_file="create/table.md" ;; - "create last_cache") expected_file="create/last_cache.md" ;; - "create distinct_cache") expected_file="create/distinct_cache.md" ;; - "show databases") expected_file="show/databases.md" ;; - "show tokens") expected_file="show/tokens.md" ;; - "delete database") expected_file="delete/database.md" ;; - "delete table") expected_file="delete/table.md" ;; - "query") expected_file="query.md" ;; - "write") expected_file="write.md" ;; - *) continue ;; - esac - - if [ -n "$expected_file" ]; then - # Check both product-specific and shared docs - local product_file="$docs_path/$expected_file" - local shared_file="$shared_path/$expected_file" - - if [ ! -f "$product_file" ] && [ ! -f "$shared_file" ]; then - echo "- **Missing**: Documentation for \`influxdb3 $command\`" >> "$audit_file" - echo " - Expected: \`$product_file\` or \`$shared_file\`" >> "$audit_file" - missing_count=$((missing_count + 1)) - fi - fi - done - - if [ "$missing_count" -eq 0 ]; then - echo "No missing documentation files detected." >> "$audit_file" - fi - - echo "" >> "$audit_file" - - # Check for outdated options in existing docs - echo "## Potentially Outdated Documentation" >> "$audit_file" - echo "" >> "$audit_file" - - local outdated_count=0 - - # This would require more sophisticated parsing of markdown files - # For now, we'll note this as a manual review item - echo "**Manual Review Needed**: Compare the following CLI options with existing documentation:" >> "$audit_file" - echo "" >> "$audit_file" - - # Extract all options from CLI help - grep -E "^\s*(-[a-zA-Z],?\s*)?--[a-z][a-z0-9-]*" "$cli_file" | sort -u | while read -r option_line; do - local option=$(echo "$option_line" | grep -oE '\--[a-z][a-z0-9-]*') - if [ -n "$option" ]; then - echo "- \`$option\`" >> "$audit_file" - fi - done - - echo "" >> "$audit_file" - echo "## Summary" >> "$audit_file" - echo "- Missing documentation files: $missing_count" >> "$audit_file" - echo "- Manual review recommended for option accuracy" >> "$audit_file" - echo "" >> "$audit_file" - - echo "📄 Audit complete: $audit_file" -} - -# Main execution -case "$PRODUCT" in - "core") - CLI_FILE="$OUTPUT_DIR/current-cli-core-${VERSION}.txt" - AUDIT_FILE="$OUTPUT_DIR/documentation-audit-core-${VERSION}.md" - - extract_current_cli "core" "$CLI_FILE" - audit_docs "core" "$CLI_FILE" "$AUDIT_FILE" - ;; - "enterprise") - CLI_FILE="$OUTPUT_DIR/current-cli-enterprise-${VERSION}.txt" - AUDIT_FILE="$OUTPUT_DIR/documentation-audit-enterprise-${VERSION}.md" - - extract_current_cli "enterprise" "$CLI_FILE" - audit_docs "enterprise" "$CLI_FILE" "$AUDIT_FILE" - ;; - "both") - # Core - CLI_FILE_CORE="$OUTPUT_DIR/current-cli-core-${VERSION}.txt" - AUDIT_FILE_CORE="$OUTPUT_DIR/documentation-audit-core-${VERSION}.md" - - extract_current_cli "core" "$CLI_FILE_CORE" - audit_docs "core" "$CLI_FILE_CORE" "$AUDIT_FILE_CORE" - - # Enterprise - CLI_FILE_ENT="$OUTPUT_DIR/current-cli-enterprise-${VERSION}.txt" - AUDIT_FILE_ENT="$OUTPUT_DIR/documentation-audit-enterprise-${VERSION}.md" - - extract_current_cli "enterprise" "$CLI_FILE_ENT" - audit_docs "enterprise" "$CLI_FILE_ENT" "$AUDIT_FILE_ENT" - ;; - *) - echo "Usage: $0 [core|enterprise|both] [version]" - exit 1 - ;; -esac - -echo "" -echo -e "${GREEN}✅ CLI documentation audit complete!${NC}" -echo "" -echo "Next steps:" -echo "1. Review the audit reports in: $OUTPUT_DIR" -echo "2. Update missing documentation files" -echo "3. Verify options match current CLI behavior" -echo "4. Update examples and usage patterns" \ No newline at end of file diff --git a/package.json b/package.json index 741ff4dba..4619192a8 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,12 @@ "test:links:telegraf": "node cypress/support/run-e2e-specs.js --spec \"cypress/e2e/content/article-links.cy.js\" content/telegraf/**/*.{md,html}", "test:links:shared": "node cypress/support/run-e2e-specs.js --spec \"cypress/e2e/content/article-links.cy.js\" content/shared/**/*.{md,html}", "test:links:api-docs": "node cypress/support/run-e2e-specs.js --spec \"cypress/e2e/content/article-links.cy.js\" /influxdb3/core/api/,/influxdb3/enterprise/api/,/influxdb3/cloud-dedicated/api/,/influxdb3/cloud-dedicated/api/v1/,/influxdb/cloud-dedicated/api/v1/,/influxdb/cloud-dedicated/api/management/,/influxdb3/cloud-dedicated/api/management/", - "test:shortcode-examples": "node cypress/support/run-e2e-specs.js --spec \"cypress/e2e/content/article-links.cy.js\" content/example.md" + "test:shortcode-examples": "node cypress/support/run-e2e-specs.js --spec \"cypress/e2e/content/article-links.cy.js\" content/example.md", + "audit:cli": "node ./helper-scripts/influxdb3-monolith/audit-cli-documentation.js both local", + "audit:cli:3core": "node ./helper-scripts/influxdb3-monolith/audit-cli-documentation.js core local", + "audit:cli:3ent": "node ./helper-scripts/influxdb3-monolith/audit-cli-documentation.js enterprise local", + "audit:cli:apply": "node ./helper-scripts/influxdb3-monolith/apply-cli-patches.js both", + "audit:cli:apply:dry": "node ./helper-scripts/influxdb3-monolith/apply-cli-patches.js both --dry-run" }, "type": "module", "browserslist": [