/** * Centralized version calculation utility * Used by both /api/version endpoint and UpdateManager to ensure consistency */ const { execSync } = require('child_process'); const path = require('path'); /** * Calculate the current version dynamically from git * @returns {Object} Version information including version, branch, and isDevelopment */ function getCurrentVersionInfo() { try { const packageJson = require('../package.json'); let currentVersion = packageJson.version || 'N/A'; let gitBranch = null; let isDevelopment = false; // Try to detect git branch and calculate dynamic version try { const gitDir = path.join(__dirname, '..'); // Get current branch gitBranch = execSync('git branch --show-current', { cwd: gitDir, encoding: 'utf8' }).trim(); // If on develop branch, calculate RC version from git if (gitBranch === 'develop') { isDevelopment = true; try { // Get the latest stable release tag const latestStableTag = execSync('git tag -l "v*" | grep -v "rc\\\\|alpha\\\\|beta" | sort -V | tail -1', { cwd: gitDir, encoding: 'utf8', shell: '/bin/bash' }).trim(); if (latestStableTag) { // Remove 'v' prefix to get base version const baseVersion = latestStableTag.replace(/^v/, ''); // Count commits since the latest stable tag const commitsSince = execSync(`git rev-list --count ${latestStableTag}..HEAD`, { cwd: gitDir, encoding: 'utf8' }).trim(); const commitsCount = parseInt(commitsSince, 10); if (commitsCount > 0) { // Calculate RC version: base version + rc + commit count currentVersion = `${baseVersion}-rc${commitsCount}`; } else { // No commits since stable, use base version currentVersion = baseVersion; } } } catch (versionError) { console.log('[VersionUtils] Could not calculate RC version from git, using package.json'); // Fall back to package.json version currentVersion = packageJson.version; } } } catch (gitError) { // Git not available or not a git repo gitBranch = null; currentVersion = packageJson.version; } return { version: currentVersion, gitBranch: gitBranch, isDevelopment: isDevelopment || gitBranch === 'develop' || process.env.NODE_ENV === 'development' }; } catch (error) { console.warn('[VersionUtils] Error getting current version:', error.message); const packageJson = require('../package.json'); return { version: packageJson.version || 'N/A', gitBranch: null, isDevelopment: false }; } } /** * Get just the version string (for backwards compatibility) * @returns {string} The current version */ function getCurrentVersion() { return getCurrentVersionInfo().version; } /** * Analyze commits since the last stable release to suggest version bump * @returns {Object} Analysis with suggested version bump and reasoning */ function analyzeCommitsForVersionBump() { try { const { execSync } = require('child_process'); const packageJson = require('../package.json'); const gitDir = path.join(__dirname, '..'); // Get the latest stable release tag (no RC/alpha/beta) let latestStableTag; try { latestStableTag = execSync('git tag -l "v*" | grep -v "rc\\|alpha\\|beta" | sort -V | tail -1', { cwd: gitDir, encoding: 'utf8', shell: '/bin/bash' }).trim(); } catch (error) { // No stable tags found, use v0.0.0 as baseline latestStableTag = 'v0.0.0'; } if (!latestStableTag) { latestStableTag = 'v0.0.0'; } // Get commit messages since last stable release let commitMessages; try { commitMessages = execSync(`git log ${latestStableTag}..HEAD --pretty=format:"%s"`, { cwd: gitDir, encoding: 'utf8' }).trim(); } catch (error) { // If git log fails, assume no commits commitMessages = ''; } if (!commitMessages) { return { currentStableVersion: latestStableTag.replace(/^v/, ''), suggestedVersion: packageJson.version, bumpType: 'none', reasoning: 'No commits since last stable release', commits: [] }; } const commits = commitMessages.split('\n').filter(msg => msg.trim()); // Analyze commit types const analysis = { breaking: [], features: [], fixes: [], other: [] }; commits.forEach(commit => { const msg = commit.toLowerCase(); // Check for breaking changes if (commit.includes('BREAKING CHANGE') || commit.includes('!:')) { analysis.breaking.push(commit); } // Check for features else if (msg.startsWith('feat:') || msg.startsWith('feature:')) { analysis.features.push(commit); } // Check for fixes else if (msg.startsWith('fix:') || msg.startsWith('bugfix:')) { analysis.fixes.push(commit); } // Everything else else { analysis.other.push(commit); } }); // Determine version bump type let bumpType = 'patch'; let reasoning = ''; if (analysis.breaking.length > 0) { bumpType = 'major'; reasoning = `Major bump due to ${analysis.breaking.length} breaking change(s)`; } else if (analysis.features.length > 0) { bumpType = 'minor'; reasoning = `Minor bump due to ${analysis.features.length} new feature(s)`; } else if (analysis.fixes.length > 0) { bumpType = 'patch'; reasoning = `Patch bump due to ${analysis.fixes.length} bug fix(es)`; } else { bumpType = 'patch'; reasoning = `Patch bump for ${analysis.other.length} other change(s)`; } // Calculate suggested version const currentStableVersion = latestStableTag.replace(/^v/, ''); const suggestedVersion = calculateNextVersion(currentStableVersion, bumpType); return { currentStableVersion, suggestedVersion, bumpType, reasoning, commits: commits, analysis, totalCommits: commits.length }; } catch (error) { console.warn('[VersionUtils] Error analyzing commits for version bump:', error.message); const packageJson = require('../package.json'); return { currentStableVersion: packageJson.version, suggestedVersion: packageJson.version, bumpType: 'none', reasoning: 'Error analyzing commits', commits: [] }; } } /** * Calculate the next version based on current version and bump type * @param {string} currentVersion - Current semantic version (e.g., "3.24.0") * @param {string} bumpType - Type of bump: major, minor, or patch * @returns {string} Next version */ function calculateNextVersion(currentVersion, bumpType) { try { // Parse current version const versionMatch = currentVersion.match(/^(\d+)\.(\d+)\.(\d+)/); if (!versionMatch) { throw new Error(`Invalid version format: ${currentVersion}`); } let [, major, minor, patch] = versionMatch.map(Number); switch (bumpType) { case 'major': major += 1; minor = 0; patch = 0; break; case 'minor': minor += 1; patch = 0; break; case 'patch': patch += 1; break; default: // No bump break; } return `${major}.${minor}.${patch}`; } catch (error) { console.warn('[VersionUtils] Error calculating next version:', error.message); return currentVersion; } } /** * Check if current branch should trigger a stable release * (i.e., we're on main branch and last commit was a merge from develop) * @returns {boolean} True if this should trigger a stable release */ function shouldTriggerStableRelease() { try { const { execSync } = require('child_process'); const gitDir = path.join(__dirname, '..'); // Check if we're on main branch const currentBranch = execSync('git branch --show-current', { cwd: gitDir, encoding: 'utf8' }).trim(); if (currentBranch !== 'main') { return false; } // Check if the last commit was a merge from develop try { const lastCommitMessage = execSync('git log -1 --pretty=format:"%s"', { cwd: gitDir, encoding: 'utf8' }).trim(); // Look for merge commit patterns from develop const isMergeFromDevelop = lastCommitMessage.includes('Merge branch \'develop\'') || lastCommitMessage.includes('Merge pull request') || lastCommitMessage.includes('develop'); return isMergeFromDevelop; } catch (error) { return false; } } catch (error) { console.warn('[VersionUtils] Error checking for stable release trigger:', error.message); return false; } } module.exports = { getCurrentVersionInfo, getCurrentVersion, analyzeCommitsForVersionBump, calculateNextVersion, shouldTriggerStableRelease };