UNPKG

bumper-cli

Version:

šŸš€ A magical release management system with beautiful changelogs and automated workflows

188 lines • 6.98 kB
#!/usr/bin/env node import { execSync } from 'node:child_process'; import chalk from 'chalk'; // Conventional commit regex pattern const CONVENTIONAL_COMMIT_REGEX = /^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert|security)(\([\w-]+\))?(!)?:\s.+$/; // Parse commit message const parseCommitMessage = (message) => { const match = message.match(CONVENTIONAL_COMMIT_REGEX); if (!match) { return { type: 'invalid', breaking: false, subject: message, }; } const [, commitType, commitScope, isBreaking] = match; const subject = message.substring(message.indexOf(':') + 1).trim(); return { type: commitType || 'invalid', scope: commitScope ? commitScope.slice(1, -1) : undefined, breaking: !!isBreaking, subject, }; }; // Check for common commit issues const checkCommonIssues = (message) => { const warnings = []; if (message.toLowerCase().includes('wip')) { warnings.push('Commit message contains "WIP" - consider squashing before release'); } if (message.toLowerCase().includes('fixup')) { warnings.push('Commit message contains "fixup" - consider squashing before release'); } return warnings; }; // Validate commit structure const validateCommitStructure = (parsed) => { const errors = []; if (parsed.type === 'invalid') { errors.push('Invalid commit type'); } if (parsed.subject.length === 0) { errors.push('Commit subject is empty'); } return errors; }; // Validate commit subject const validateCommitSubject = (parsed) => { const warnings = []; if (parsed.subject.endsWith('.')) { warnings.push('Commit subject ends with a period'); } return warnings; }; // Validate single commit message const validateCommitMessage = (message) => { const errors = []; const warnings = []; // Check if message follows conventional commit format if (!CONVENTIONAL_COMMIT_REGEX.test(message)) { errors.push('Commit message does not follow conventional commit format'); } // Check message length if (message.length > 72) { warnings.push('Commit message is longer than 72 characters'); } // Check for common issues warnings.push(...checkCommonIssues(message)); // Parse and validate structure const parsed = parseCommitMessage(message); errors.push(...validateCommitStructure(parsed)); warnings.push(...validateCommitSubject(parsed)); return { isValid: errors.length === 0, errors, warnings, }; }; // Parse commit line from git log const parseCommitLine = (commitLine) => { const commitParts = commitLine.split('|'); const commitHash = commitParts[0] || ''; const commitMessage = commitParts[1] || ''; return { hash: commitHash.substring(0, 8), message: commitMessage, }; }; // Get commits from git log const getCommitsFromGitLog = (range) => { const command = range ? `git log --pretty=format:"%H|%s" ${range}` : 'git log --pretty=format:"%H|%s"'; const commits = execSync(command, { encoding: 'utf8' }).trim(); if (!commits) return []; return commits.split('\n').map(parseCommitLine); }; // Get commits since last tag const getCommitsSinceLastTag = () => { try { const lastTag = execSync('git describe --tags --abbrev=0', { encoding: 'utf8', }).trim(); return getCommitsFromGitLog(`${lastTag}..HEAD`); } catch { // If no tags exist, get all commits return getCommitsFromGitLog(); } }; // Display validation result for a single commit const displayCommitValidation = (hash, message, validationResult) => { let errors = 0; let warnings = 0; if (validationResult.errors.length > 0) { errors = validationResult.errors.length; console.log(chalk.red(`āŒ ${hash}: ${message}`)); for (const errorMessage of validationResult.errors) { console.log(chalk.red(` - ${errorMessage}`)); } } else if (validationResult.warnings.length > 0) { warnings = validationResult.warnings.length; console.log(chalk.yellow(`āš ļø ${hash}: ${message}`)); for (const warningMessage of validationResult.warnings) { console.log(chalk.yellow(` - ${warningMessage}`)); } } else { console.log(chalk.green(`āœ… ${hash}: ${message}`)); } return { errors, warnings }; }; // Display validation summary const displayValidationSummary = (commits, results, totalErrors, totalWarnings) => { console.log('\nšŸ“Š Validation Summary:'); console.log(`Total commits: ${chalk.blue(commits.length)}`); console.log(`Valid commits: ${chalk.green(commits.length - results.filter((result) => !result.isValid).length)}`); console.log(`Invalid commits: ${chalk.red(results.filter((result) => !result.isValid).length)}`); console.log(`Total errors: ${chalk.red(totalErrors)}`); console.log(`Total warnings: ${chalk.yellow(totalWarnings)}`); }; // Display final validation message const displayFinalMessage = (totalErrors, totalWarnings) => { if (totalErrors > 0) { console.log(chalk.red('\nāŒ Validation failed! Please fix the errors above.')); console.log(chalk.blue('šŸ’” Tip: Use "git commit --amend" to fix recent commits')); } else if (totalWarnings > 0) { console.log(chalk.yellow('\nāš ļø Validation passed with warnings.')); } else { console.log(chalk.green('\nāœ… All commits are valid!')); } }; // Main validation function export const validateCommits = async () => { console.log(chalk.blue('šŸ” Validating commit messages...')); const commits = getCommitsSinceLastTag(); if (commits.length === 0) { console.log(chalk.yellow('āš ļø No commits found to validate.')); return { isValid: true, errors: [], warnings: [] }; } console.log(chalk.green(`šŸ“ Validating ${commits.length} commits...`)); const results = []; let totalErrors = 0; let totalWarnings = 0; for (const { hash, message } of commits) { const validationResult = validateCommitMessage(message); results.push(validationResult); const { errors, warnings } = displayCommitValidation(hash, message, validationResult); totalErrors += errors; totalWarnings += warnings; } displayValidationSummary(commits, results, totalErrors, totalWarnings); const allErrors = results.flatMap((result) => result.errors); const allWarnings = results.flatMap((result) => result.warnings); displayFinalMessage(totalErrors, totalWarnings); return { isValid: totalErrors === 0, errors: allErrors, warnings: allWarnings, }; }; // Export for use in other modules export { validateCommitMessage, parseCommitMessage }; //# sourceMappingURL=validateCommits.js.map