#!/usr/bin/env node import { execSync } from 'child_process'; import { writeFileSync, mkdirSync, existsSync, readFileSync } from 'fs'; import { dirname, join } from 'path'; import { fileURLToPath } from 'url'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); // Colors for console output const colors = { red: '\x1b[0;31m', green: '\x1b[0;32m', yellow: '\x1b[0;33m', blue: '\x1b[0;34m', nc: '\x1b[0m', // No Color }; // Default configuration const DEFAULT_CONFIG = { outputFormat: 'integrated', // 'integrated' or 'separated' primaryRepo: null, // Index or name of primary repository (for separated format) repositories: [ { name: 'primary', path: null, // Will be set from command line label: 'primary', includePrLinks: true, // Default to include PR links }, ], // Template for separated format separatedTemplate: { header: null, // Optional header text/markdown primaryLabel: 'Primary', // Label for primary section secondaryLabel: 'Additional Changes', // Label for secondary section secondaryIntro: 'All primary updates are included. Additional repository-specific features and fixes:', // Intro text for secondary }, }; class ReleaseNotesGenerator { constructor(options = {}) { this.fromVersion = options.fromVersion || 'v3.1.0'; this.toVersion = options.toVersion || 'v3.2.0'; this.fetchCommits = options.fetchCommits !== false; this.pullCommits = options.pullCommits || false; this.includePrLinks = options.includePrLinks !== false; // Default to true this.config = options.config || DEFAULT_CONFIG; this.outputDir = options.outputDir || join(__dirname, '..', 'output', 'release-notes'); } log(message, color = 'nc') { console.log(`${colors[color]}${message}${colors.nc}`); } // Validate git tag exists in repository validateGitTag(version, repoPath) { if (version === 'local') { return true; // Special case for development } if (!existsSync(repoPath)) { this.log(`Error: Repository not found: ${repoPath}`, 'red'); return false; } try { const tags = execSync(`git -C "${repoPath}" tag --list`, { encoding: 'utf8', }); if (!tags.split('\n').includes(version)) { this.log( `Error: Version tag '${version}' does not exist in repository ${repoPath}`, 'red' ); this.log('Available tags (most recent first):', 'yellow'); const recentTags = execSync( `git -C "${repoPath}" tag --list --sort=-version:refname`, { encoding: 'utf8' } ) .split('\n') .slice(0, 10) .filter((tag) => tag.trim()) .map((tag) => ` ${tag}`) .join('\n'); console.log(recentTags); return false; } return true; } catch (error) { this.log(`Error validating tags in ${repoPath}: ${error.message}`, 'red'); return false; } } // Get commits from repository using subject line pattern getCommitsFromRepo(repoPath, pattern, format = '%h %s') { try { const output = execSync( `git -C "${repoPath}" log --format="${format}" "${this.fromVersion}..${this.toVersion}"`, { encoding: 'utf8' } ); return output .split('\n') .filter((line) => line.match(new RegExp(pattern))) .filter((line) => line.trim()); } catch { return []; } } // Get commits including merge commit bodies getCommitsWithBody(repoPath, pattern) { try { const output = execSync( `git -C "${repoPath}" log --format="%B" "${this.fromVersion}..${this.toVersion}"`, { encoding: 'utf8' } ); // Split into lines and find lines that match the pattern const lines = output.split('\n'); const matches = []; for (const line of lines) { const trimmedLine = line.trim(); if ( trimmedLine.startsWith(pattern) || trimmedLine.startsWith('* ' + pattern) || trimmedLine.startsWith('- ' + pattern) ) { // Remove the bullet point prefix if present const cleanLine = trimmedLine.replace(/^[*-]\s*/, ''); if (cleanLine.length > pattern.length) { matches.push(cleanLine); } } } return matches; } catch { return []; } } // Extract PR number from commit message extractPrNumber(message) { const match = message.match(/#(\d+)/); return match ? match[1] : null; } // Configuration for enhancing commit messages getEnhancementConfig() { return { // Keywords to detect different areas of the codebase detectors: { auth: ['auth', 'token', 'permission', 'credential', 'security'], database: ['database', 'table', 'schema', 'catalog'], query: ['query', 'sql', 'select', 'influxql'], storage: ['storage', 'parquet', 'wal', 'object store'], license: ['license', 'licensing'], compaction: ['compact', 'compaction'], cache: ['cache', 'caching', 'lru'], metrics: ['metric', 'monitoring', 'telemetry'], retention: ['retention', 'ttl', 'expire'], api: ['api', 'endpoint', 'http', 'rest'], cli: ['cli', 'command', 'cmd', 'flag'], }, // Feature type mappings featureTypes: { feat: { 'auth+database': 'Enhanced database authorization', auth: 'Authentication and security', 'database+retention': 'Database retention management', database: 'Database management', query: 'Query functionality', 'storage+compaction': 'Storage compaction', storage: 'Storage engine', license: 'License management', cache: 'Caching system', metrics: 'Monitoring and metrics', cli: 'Command-line interface', api: 'API functionality', }, fix: { auth: 'Authentication fix', database: 'Database reliability', query: 'Query processing', storage: 'Storage integrity', compaction: 'Compaction stability', cache: 'Cache reliability', license: 'License validation', cli: 'CLI reliability', api: 'API stability', _default: 'Bug fix', }, perf: { query: 'Query performance', storage: 'Storage performance', compaction: 'Compaction performance', cache: 'Cache performance', _default: 'Performance improvement', }, }, // Feature name extraction patterns featurePatterns: { 'delete|deletion': 'Data deletion', retention: 'Retention policies', 'token|auth': 'Authentication', 'database|db': 'Database management', table: 'Table operations', query: 'Query engine', cache: 'Caching', 'metric|monitoring': 'Monitoring', license: 'Licensing', 'compaction|compact': 'Storage compaction', wal: 'Write-ahead logging', parquet: 'Parquet storage', api: 'API', 'cli|command': 'CLI', }, }; } // Detect areas based on keywords in the description detectAreas(description, files = []) { const config = this.getEnhancementConfig(); const lowerDesc = description.toLowerCase(); const detectedAreas = new Set(); // Check description for keywords for (const [area, keywords] of Object.entries(config.detectors)) { if (keywords.some((keyword) => lowerDesc.includes(keyword))) { detectedAreas.add(area); } } // Check files for patterns const filePatterns = { auth: ['auth/', 'security/', 'token/'], database: ['database/', 'catalog/', 'schema/'], query: ['query/', 'sql/', 'influxql/'], storage: ['storage/', 'parquet/', 'wal/'], api: ['api/', 'http/', 'rest/'], cli: ['cli/', 'cmd/', 'command/'], metrics: ['metrics/', 'telemetry/', 'monitoring/'], cache: ['cache/', 'lru/'], }; for (const [area, patterns] of Object.entries(filePatterns)) { if ( files.some((file) => patterns.some((pattern) => file.includes(pattern))) ) { detectedAreas.add(area); } } return Array.from(detectedAreas); } // Get enhancement label based on type and detected areas getEnhancementLabel(type, areas) { const config = this.getEnhancementConfig(); const typeConfig = config.featureTypes[type]; if (!typeConfig) { return this.capitalizeFirst(type); } // Check for multi-area combinations first if (areas.length > 1) { const comboKey = areas.slice(0, 2).sort().join('+'); if (typeConfig[comboKey]) { return typeConfig[comboKey]; } } // Check for single area match if (areas.length > 0 && typeConfig[areas[0]]) { return typeConfig[areas[0]]; } // Return default if available return typeConfig._default || this.capitalizeFirst(type); } // Extract feature name using patterns extractFeatureName(description) { const config = this.getEnhancementConfig(); const words = description.toLowerCase(); // Check each pattern for (const [pattern, featureName] of Object.entries( config.featurePatterns )) { const regex = new RegExp(`\\b(${pattern})\\b`, 'i'); if (regex.test(words)) { return featureName; } } // Default to extracting the first significant word const significantWords = words .split(' ') .filter( (w) => w.length > 3 && ![ 'the', 'and', 'for', 'with', 'from', 'into', 'that', 'this', ].includes(w) ); return significantWords.length > 0 ? this.capitalizeFirst(significantWords[0]) : 'Feature'; } // Get detailed information about a commit including files changed getCommitDetails(repoPath, commitHash) { try { const output = execSync( `git -C "${repoPath}" show --name-only --format="%s%n%b" ${commitHash}`, { encoding: 'utf8' } ); const lines = output.split('\n'); const subject = lines[0]; let bodyLines = []; let fileLines = []; let inBody = true; for (let i = 1; i < lines.length; i++) { const line = lines[i].trim(); if (line === '') continue; // If we hit a file path, we're done with the body if (line.includes('/') || line.includes('.')) { inBody = false; } if (inBody) { bodyLines.push(line); } else { fileLines.push(line); } } return { subject, body: bodyLines.join('\n'), files: fileLines, }; } catch { return null; } } // Enhance commit message with analysis of changes enhanceCommitMessage( repoPath, commitMessage, prNumber, includePrLinks = null ) { // Extract the basic semantic prefix const semanticMatch = commitMessage.match( /^(feat|fix|perf|refactor|style|test|docs|chore):\s*(.+)/ ); if (!semanticMatch) return commitMessage; const [, type, description] = semanticMatch; // Remove PR number from description if it's already there to avoid duplication const cleanDescription = description.replace(/\s*\(#\d+\)$/g, '').trim(); // Get commit hash if available const hashMatch = commitMessage.match(/^([a-f0-9]+)\s+/); const commitHash = hashMatch ? hashMatch[1] : null; // Try to enhance based on the type and description const enhanced = this.generateEnhancedDescription( type, cleanDescription, repoPath, commitHash ); // Use repository-specific setting if provided, otherwise use global setting const shouldIncludePrLinks = includePrLinks !== null ? includePrLinks : this.includePrLinks; // If we have a PR number and should include PR links, include it if (prNumber && shouldIncludePrLinks) { return `${enhanced} ([#${prNumber}](https://github.com/influxdata/influxdb/pull/${prNumber}))`; } return enhanced; } // Generate enhanced description based on commit type and analysis generateEnhancedDescription(type, description, repoPath, commitHash) { // Get additional context if commit hash is available let files = []; if (commitHash) { const details = this.getCommitDetails(repoPath, commitHash); if (details) { files = details.files; } } // Detect areas affected by this commit const areas = this.detectAreas(description, files); // Get the enhancement label const label = this.getEnhancementLabel(type, areas); // For features without detected areas, try to extract a feature name if (type === 'feat' && areas.length === 0) { const featureName = this.extractFeatureName(description); return `**${featureName}**: ${this.capitalizeFirst(description)}`; } return `**${label}**: ${this.capitalizeFirst(description)}`; } // Capitalize first letter of a string capitalizeFirst(str) { if (!str) return ''; return str.charAt(0).toUpperCase() + str.slice(1); } // Get release date getReleaseDate(repoPath) { try { const output = execSync( `git -C "${repoPath}" log -1 --format=%ai "${this.toVersion}"`, { encoding: 'utf8' } ); return output.split(' ')[0].trim(); } catch { return new Date().toISOString().split('T')[0]; } } // Fetch latest commits from repositories async fetchFromRepositories() { if (!this.fetchCommits) { this.log('Skipping fetch (using local commits only)', 'yellow'); return; } const action = this.pullCommits ? 'Pulling' : 'Fetching'; this.log(`${action} latest commits from all repositories...`, 'yellow'); if (this.pullCommits) { this.log('Warning: This will modify your working directories!', 'red'); } for (const repo of this.config.repositories) { if (!existsSync(repo.path)) { this.log(`✗ Repository not found: ${repo.path}`, 'red'); continue; } const repoName = repo.name || repo.path.split('/').pop(); try { if (this.pullCommits) { this.log(` Pulling changes in ${repoName}...`); execSync(`git -C "${repo.path}" pull origin`, { stdio: 'pipe' }); this.log(` ✓ Successfully pulled changes in ${repoName}`, 'green'); } else { this.log(` Fetching from ${repoName}...`); execSync(`git -C "${repo.path}" fetch origin`, { stdio: 'pipe' }); this.log(` ✓ Successfully fetched from ${repoName}`, 'green'); } } catch { this.log( ` ✗ Failed to ${this.pullCommits ? 'pull' : 'fetch'} from ${repoName}`, 'red' ); } } } // Collect commits by category from all repositories collectCommits() { this.log('\nAnalyzing commits across all repositories...', 'yellow'); const results = { features: [], fixes: [], breaking: [], perf: [], api: [], }; for (const repo of this.config.repositories) { if (!existsSync(repo.path)) { continue; } const repoLabel = repo.label || repo.name || repo.path.split('/').pop(); this.log(` Analyzing ${repoLabel}...`); // Features - check both commit subjects and merge commit bodies const featuresSubject = this.getCommitsFromRepo( repo.path, '^[a-f0-9]+ feat:' ).map((line) => { const prNumber = this.extractPrNumber(line); const enhanced = this.enhanceCommitMessage( repo.path, line.replace(/^[a-f0-9]* /, ''), prNumber, repo.includePrLinks ); return `- [${repoLabel}] ${enhanced}`; }); const featuresBody = this.getCommitsWithBody(repo.path, 'feat:').map( (line) => { const prNumber = this.extractPrNumber(line); const enhanced = this.enhanceCommitMessage( repo.path, line, prNumber, repo.includePrLinks ); return `- [${repoLabel}] ${enhanced}`; } ); results.features.push(...featuresSubject, ...featuresBody); // Fixes - check both commit subjects and merge commit bodies const fixesSubject = this.getCommitsFromRepo( repo.path, '^[a-f0-9]+ fix:' ).map((line) => { const prNumber = this.extractPrNumber(line); const enhanced = this.enhanceCommitMessage( repo.path, line.replace(/^[a-f0-9]* /, ''), prNumber, repo.includePrLinks ); return `- [${repoLabel}] ${enhanced}`; }); const fixesBody = this.getCommitsWithBody(repo.path, 'fix:').map( (line) => { const prNumber = this.extractPrNumber(line); const enhanced = this.enhanceCommitMessage( repo.path, line, prNumber, repo.includePrLinks ); return `- [${repoLabel}] ${enhanced}`; } ); results.fixes.push(...fixesSubject, ...fixesBody); // Performance improvements const perfSubject = this.getCommitsFromRepo( repo.path, '^[a-f0-9]+ perf:' ).map((line) => { const prNumber = this.extractPrNumber(line); const enhanced = this.enhanceCommitMessage( repo.path, line.replace(/^[a-f0-9]* /, ''), prNumber, repo.includePrLinks ); return `- [${repoLabel}] ${enhanced}`; }); const perfBody = this.getCommitsWithBody(repo.path, 'perf:').map( (line) => { const prNumber = this.extractPrNumber(line); const enhanced = this.enhanceCommitMessage( repo.path, line, prNumber, repo.includePrLinks ); return `- [${repoLabel}] ${enhanced}`; } ); results.perf.push(...perfSubject, ...perfBody); // Breaking changes const breaking = this.getCommitsFromRepo( repo.path, '^[a-f0-9]+ .*(BREAKING|breaking change)' ).map((line) => line.replace(/^[a-f0-9]* /, `- [${repoLabel}] `)); results.breaking.push(...breaking); // API changes const api = this.getCommitsFromRepo( repo.path, '(api|endpoint|/write|/query|/ping|/health|/metrics|v1|v2|v3)' ).map((line) => line.replace(/^[a-f0-9]* /, `- [${repoLabel}] `)); results.api.push(...api); } return results; } // Generate integrated format release notes generateIntegratedFormat(commits, releaseDate) { const lines = []; lines.push(`## ${this.toVersion} {date="${releaseDate}"}`); lines.push(''); lines.push('### Features'); lines.push(''); if (commits.features.length > 0) { commits.features.forEach((feature) => { // Enhanced messages already include PR links lines.push(feature); }); } else { lines.push('- No new features in this release'); } lines.push(''); lines.push('### Bug Fixes'); lines.push(''); if (commits.fixes.length > 0) { commits.fixes.forEach((fix) => { // Enhanced messages already include PR links lines.push(fix); }); } else { lines.push('- No bug fixes in this release'); } // Add breaking changes if any if (commits.breaking.length > 0) { lines.push(''); lines.push('### Breaking Changes'); lines.push(''); commits.breaking.forEach((change) => { const pr = this.extractPrNumber(change); const cleanLine = change.replace(/ \\(#\\d+\\)$/, ''); if (pr && this.includePrLinks) { lines.push( `${cleanLine} ([#${pr}](https://github.com/influxdata/influxdb/pull/${pr}))` ); } else { lines.push(cleanLine); } }); } // Add performance improvements if any if (commits.perf.length > 0) { lines.push(''); lines.push('### Performance Improvements'); lines.push(''); commits.perf.forEach((perf) => { // Enhanced messages already include PR links lines.push(perf); }); } // Add HTTP API changes if any if (commits.api.length > 0) { lines.push(''); lines.push('### HTTP API Changes'); lines.push(''); commits.api.forEach((api) => { const pr = this.extractPrNumber(api); const cleanLine = api.replace(/ \\(#\\d+\\)$/, ''); if (pr && this.includePrLinks) { lines.push( `${cleanLine} ([#${pr}](https://github.com/influxdata/influxdb/pull/${pr}))` ); } else { lines.push(cleanLine); } }); } // Add API analysis summary lines.push(''); lines.push('### API Analysis Summary'); lines.push(''); lines.push( 'The following endpoints may have been affected in this release:' ); lines.push('- v1 API endpoints: `/write`, `/query`, `/ping`'); lines.push('- v2 API endpoints: `/api/v2/write`, `/api/v2/query`'); lines.push('- v3 API endpoints: `/api/v3/*`'); lines.push('- System endpoints: `/health`, `/metrics`'); lines.push(''); lines.push( 'Please review the commit details above and consult the API documentation for specific changes.' ); lines.push(''); return lines.join('\n'); } // Generate separated format release notes generateSeparatedFormat(commits, releaseDate) { const lines = []; // Add custom header if provided if (this.config.separatedTemplate && this.config.separatedTemplate.header) { lines.push(this.config.separatedTemplate.header); lines.push(''); } lines.push(`## ${this.toVersion} {date="${releaseDate}"}`); lines.push(''); // Determine primary repository let primaryRepoLabel = null; if (this.config.primaryRepo !== null) { // Find primary repo by index or name if (typeof this.config.primaryRepo === 'number') { const primaryRepo = this.config.repositories[this.config.primaryRepo]; primaryRepoLabel = primaryRepo ? primaryRepo.label : null; } else { const primaryRepo = this.config.repositories.find( (r) => r.name === this.config.primaryRepo ); primaryRepoLabel = primaryRepo ? primaryRepo.label : null; } } // If no primary specified, use the first repository if (!primaryRepoLabel && this.config.repositories.length > 0) { primaryRepoLabel = this.config.repositories[0].label; } // Separate commits by primary and secondary repositories const primaryCommits = { features: [], fixes: [], perf: [], }; const secondaryCommits = { features: [], fixes: [], perf: [], }; // Sort commits into primary and secondary for (const type of ['features', 'fixes', 'perf']) { commits[type].forEach((commit) => { // Extract repository label from commit const labelMatch = commit.match(/^- \[([^\]]+)\]/); if (labelMatch) { const repoLabel = labelMatch[1]; const cleanCommit = commit.replace(/^- \[[^\]]+\] /, '- '); if (repoLabel === primaryRepoLabel) { primaryCommits[type].push(cleanCommit); } else { // Keep the label for secondary commits secondaryCommits[type].push(commit); } } }); } // Primary section const primaryLabel = this.config.separatedTemplate?.primaryLabel || 'Primary'; lines.push(`### ${primaryLabel}`); lines.push(''); lines.push('#### Features'); lines.push(''); if (primaryCommits.features.length > 0) { primaryCommits.features.forEach((feature) => { lines.push(feature); }); } else { lines.push('- No new features in this release'); } lines.push(''); lines.push('#### Bug Fixes'); lines.push(''); if (primaryCommits.fixes.length > 0) { primaryCommits.fixes.forEach((fix) => { lines.push(fix); }); } else { lines.push('- No bug fixes in this release'); } // Primary performance improvements if any if (primaryCommits.perf.length > 0) { lines.push(''); lines.push('#### Performance Improvements'); lines.push(''); primaryCommits.perf.forEach((perf) => { lines.push(perf); }); } // Secondary section (only if there are secondary repositories) const hasSecondaryChanges = secondaryCommits.features.length > 0 || secondaryCommits.fixes.length > 0 || secondaryCommits.perf.length > 0; if (this.config.repositories.length > 1) { lines.push(''); const secondaryLabel = this.config.separatedTemplate?.secondaryLabel || 'Additional Changes'; lines.push(`### ${secondaryLabel}`); lines.push(''); const secondaryIntro = this.config.separatedTemplate?.secondaryIntro || 'All primary updates are included. Additional repository-specific features and fixes:'; lines.push(secondaryIntro); lines.push(''); // Secondary features if (secondaryCommits.features.length > 0) { lines.push('#### Features'); lines.push(''); secondaryCommits.features.forEach((feature) => { lines.push(feature); }); lines.push(''); } // Secondary fixes if (secondaryCommits.fixes.length > 0) { lines.push('#### Bug Fixes'); lines.push(''); secondaryCommits.fixes.forEach((fix) => { lines.push(fix); }); lines.push(''); } // Secondary performance improvements if (secondaryCommits.perf.length > 0) { lines.push('#### Performance Improvements'); lines.push(''); secondaryCommits.perf.forEach((perf) => { lines.push(perf); }); lines.push(''); } // No secondary changes message if (!hasSecondaryChanges) { lines.push('#### No additional changes'); lines.push(''); lines.push( 'All changes in this release are included in the primary repository.' ); lines.push(''); } } // Add common sections (breaking changes, API changes, etc.) this.addCommonSections(lines, commits); return lines.join('\n'); } // Add common sections (breaking changes, API analysis) addCommonSections(lines, commits) { // Add breaking changes if any if (commits.breaking.length > 0) { lines.push('### Breaking Changes'); lines.push(''); commits.breaking.forEach((change) => { const pr = this.extractPrNumber(change); const cleanLine = change.replace(/ \\(#\\d+\\)$/, ''); if (pr && this.includePrLinks) { lines.push( `${cleanLine} ([#${pr}](https://github.com/influxdata/influxdb/pull/${pr}))` ); } else { lines.push(cleanLine); } }); lines.push(''); } // Add HTTP API changes if any if (commits.api.length > 0) { lines.push('### HTTP API Changes'); lines.push(''); commits.api.forEach((api) => { const pr = this.extractPrNumber(api); const cleanLine = api.replace(/ \\(#\\d+\\)$/, ''); if (pr && this.includePrLinks) { lines.push( `${cleanLine} ([#${pr}](https://github.com/influxdata/influxdb/pull/${pr}))` ); } else { lines.push(cleanLine); } }); lines.push(''); } // Add API analysis summary lines.push('### API Analysis Summary'); lines.push(''); lines.push( 'The following endpoints may have been affected in this release:' ); lines.push('- v1 API endpoints: `/write`, `/query`, `/ping`'); lines.push('- v2 API endpoints: `/api/v2/write`, `/api/v2/query`'); lines.push('- v3 API endpoints: `/api/v3/*`'); lines.push('- System endpoints: `/health`, `/metrics`'); lines.push(''); lines.push( 'Please review the commit details above and consult the API documentation for specific changes.' ); lines.push(''); } // Generate release notes async generate() { this.log('Validating version tags...', 'yellow'); // Validate tags in primary repository const primaryRepo = this.config.repositories[0]; if ( !this.validateGitTag(this.fromVersion, primaryRepo.path) || !this.validateGitTag(this.toVersion, primaryRepo.path) ) { process.exit(1); } this.log('✓ Version tags validated successfully', 'green'); this.log(''); this.log(`Generating release notes for ${this.toVersion}`, 'blue'); this.log(`Primary Repository: ${primaryRepo.path}`); if (this.config.repositories.length > 1) { this.log('Additional Repositories:'); this.config.repositories.slice(1).forEach((repo) => { this.log(` - ${repo.path}`); }); } this.log(`From: ${this.fromVersion} To: ${this.toVersion}\n`); // Get release date from primary repository const releaseDate = this.getReleaseDate(primaryRepo.path); this.log(`Release Date: ${releaseDate}\n`, 'green'); // Fetch latest commits await this.fetchFromRepositories(); // Collect commits const commits = this.collectCommits(); // Generate output based on format let content; if (this.config.outputFormat === 'separated') { content = this.generateSeparatedFormat(commits, releaseDate); } else { content = this.generateIntegratedFormat(commits, releaseDate); } // Ensure output directory exists mkdirSync(this.outputDir, { recursive: true }); // Write output file const outputFile = join( this.outputDir, `release-notes-${this.toVersion}.md` ); writeFileSync(outputFile, content); this.log(`\nRelease notes generated in: ${outputFile}`, 'green'); this.log( 'Please review and edit the generated notes before adding to documentation.', 'yellow' ); // If running in GitHub Actions, also output the relative path if (process.env.GITHUB_WORKSPACE || process.env.GITHUB_ACTIONS) { const relativePath = outputFile.replace( `${process.env.GITHUB_WORKSPACE}/`, '' ); this.log(`\nRelative path for GitHub Actions: ${relativePath}`, 'green'); } } } // Parse command line arguments function parseArgs() { const args = process.argv.slice(2); const options = { fetchCommits: true, pullCommits: false, includePrLinks: true, config: { ...DEFAULT_CONFIG }, }; let i = 0; while (i < args.length) { switch (args[i]) { case '--no-fetch': options.fetchCommits = false; i++; break; case '--pull': options.pullCommits = true; options.fetchCommits = true; i++; break; case '--no-pr-links': options.includePrLinks = false; i++; break; case '--config': if (i + 1 >= args.length) { console.error('Error: --config requires a configuration file path'); process.exit(1); } // Load configuration from JSON file try { const configPath = args[i + 1]; const configData = JSON.parse(readFileSync(configPath, 'utf8')); options.config = { ...DEFAULT_CONFIG, ...configData }; } catch (error) { console.error(`Error loading configuration: ${error.message}`); process.exit(1); } i += 2; break; case '--format': if (i + 1 >= args.length) { console.error( 'Error: --format requires a format type (integrated|separated)' ); process.exit(1); } options.config.outputFormat = args[i + 1]; i += 2; break; case '--help': case '-h': printUsage(); process.exit(0); break; default: // Positional arguments: fromVersion toVersion primaryRepo [additionalRepos...] if (!options.fromVersion) { options.fromVersion = args[i]; } else if (!options.toVersion) { options.toVersion = args[i]; } else { // Repository paths if (!options.config.repositories[0].path) { options.config.repositories[0].path = args[i]; options.config.repositories[0].name = args[i].split('/').pop(); options.config.repositories[0].label = options.config.repositories[0].name; } else { // Additional repositories const repoName = args[i].split('/').pop(); options.config.repositories.push({ name: repoName, path: args[i], label: repoName, }); } } i++; break; } } // Set defaults options.fromVersion = options.fromVersion || 'v3.1.0'; options.toVersion = options.toVersion || 'v3.2.0'; // Set default labels if not provided options.config.repositories.forEach((repo, index) => { if (!repo.label) { repo.label = repo.name || `repo${index + 1}`; } // Set default includePrLinks if not specified if (repo.includePrLinks === undefined) { repo.includePrLinks = options.includePrLinks; } }); return options; } function printUsage() { console.log(` Usage: node generate-release-notes.js [options] [additional_repo_paths...] Options: --no-fetch Skip fetching latest commits from remote --pull Pull latest changes (implies fetch) - use with caution --no-pr-links Omit PR links from commit messages (default: include links) --config Load configuration from JSON file --format Output format: 'integrated' or 'separated' -h, --help Show this help message Examples: node generate-release-notes.js v3.1.0 v3.2.0 /path/to/influxdb node generate-release-notes.js --no-fetch v3.1.0 v3.2.0 /path/to/influxdb node generate-release-notes.js --pull v3.1.0 v3.2.0 /path/to/influxdb /path/to/influxdb_pro node generate-release-notes.js --config config.json v3.1.0 v3.2.0 node generate-release-notes.js --format separated v3.1.0 v3.2.0 /path/to/influxdb /path/to/influxdb_pro Configuration file format (JSON): { "outputFormat": "separated", "primaryRepo": "influxdb", "repositories": [ { "name": "influxdb", "path": "/path/to/influxdb", "label": "Core", "includePrLinks": true }, { "name": "influxdb_pro", "path": "/path/to/influxdb_pro", "label": "Enterprise", "includePrLinks": false } ], "separatedTemplate": { "header": "> [!Note]\\n> #### InfluxDB 3 Core and Enterprise relationship\\n>\\n> InfluxDB 3 Enterprise is a superset of InfluxDB 3 Core.\\n> All updates to Core are automatically included in Enterprise.\\n> The Enterprise sections below only list updates exclusive to Enterprise.", "primaryLabel": "Core", "secondaryLabel": "Enterprise", "secondaryIntro": "All Core updates are included in Enterprise. Additional Enterprise-specific features and fixes:" } } `); } // Main execution async function main() { try { const options = parseArgs(); const generator = new ReleaseNotesGenerator(options); await generator.generate(); } catch (error) { console.error(`Error: ${error.message}`); process.exit(1); } } // Run if called directly if (import.meta.url === `file://${process.argv[1]}`) { main(); } export { ReleaseNotesGenerator };