Files
pulse/server/versionUtils.js
T
rcourtman 75ba508c76 fix: UI update JavaScript error and installer improvements (#134)
* feat: dramatically improve automated changelog generation

- Add detailed analysis logging and error handling
- Clean up commit messages by removing prefixes (feat:, fix:, etc.)
- Group changes by type with proper emoji categories
- Include version progression info (v3.25.2 → v3.25.3)
- Add installation instructions and Docker commands
- Filter out technical/automated commits from user-facing changelog
- Provide enhanced fallback using git log if analysis fails
- Include release statistics and change counts

This replaces the generic 'Automated stable release' message with
comprehensive, user-friendly changelogs.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* chore: bump version to 3.25.2-rc3 for RC release

* fix: improve installer resilience against GitHub API rate limits

Add fallback mechanism in check_release_exists() to verify release existence
by attempting direct tarball download when GitHub API is rate limited.
This prevents installation failures when the API quota is exceeded.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: ensure install script included in releases and fix changelog generation

- Fix missing analysis property in versionUtils error fallbacks
- Improve tarball creation to always include install-pulse.sh
- Add verification step to ensure install script is included in releases
- This resolves the issue where releases were missing the installer script

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* chore: bump version to 3.25.2-rc5 for RC release

* fix: update package.json version to 3.25.5 for proper RC versioning

This ensures new RC releases are versioned ahead of the current stable release
(v3.25.4), so they appear at the top of the releases page as expected.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* chore: bump version to 3.25.5-rc1 for RC release

* fix: pass tarballAsset parameter to _performUpdate function

Fixes 'tarballAsset is not defined' error when applying updates through the UI.
The tarballAsset variable was defined in the parent scope but not passed
to the _performUpdate function.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* chore: bump version to 3.25.5-rc2 for RC release

* docs: add CLAUDE.md with git workflow and release automation notes

Documents the RC workflow behavior, common git conflicts, and testing procedures
for future development work.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* chore: bump version to 3.25.5-rc3 for RC release

* chore: bump version to 3.25.4-rc1 for RC release

---------

Co-authored-by: rcourtman <dev@pulse.local>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: GitHub Action <action@github.com>
2025-06-13 18:15:50 +01:00

327 lines
11 KiB
JavaScript

/**
* 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: [],
analysis: {
breaking: [],
features: [],
fixes: [],
other: []
},
totalCommits: 0
};
}
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: [],
analysis: {
breaking: [],
features: [],
fixes: [],
other: []
},
totalCommits: 0
};
}
}
/**
* 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
};