mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-21 18:53:37 +00:00
4fab5b3cea
- Fix frontend update functionality with proper channel switching - Add channel-aware version detection in footer - Implement proper development build versioning using git describe - Fix footer spacing issue with update indicators - Add quick channel switch buttons with immediate updates - Prevent update notifications for development builds ahead of releases - Fix release URL linking to correct channel-specific releases - Improve error handling and rate limiting for GitHub API calls The update system now properly: - Detects development builds vs release builds - Shows channel-appropriate updates only - Handles stable/RC channel switching seamlessly - Provides proper version semantics for development environments 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
343 lines
12 KiB
JavaScript
343 lines
12 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 {
|
|
// Use git describe for accurate development versioning
|
|
const gitDescribe = execSync('git describe --tags --dirty', {
|
|
cwd: gitDir,
|
|
encoding: 'utf8'
|
|
}).trim();
|
|
|
|
if (gitDescribe) {
|
|
// Parse git describe output: v3.23.1-109-g3ee29b2-dirty
|
|
const match = gitDescribe.match(/^v([^-]+)(?:-(\d+)-g([a-f0-9]+))?(-dirty)?$/);
|
|
|
|
if (match) {
|
|
const [, baseVersion, commitsAhead, shortHash, isDirty] = match;
|
|
|
|
if (commitsAhead && parseInt(commitsAhead) > 0) {
|
|
// We're ahead of the latest tag - this is development
|
|
const commits = parseInt(commitsAhead);
|
|
const dirtyFlag = isDirty ? '-dirty' : '';
|
|
|
|
// For development builds, show as dev version ahead of the base
|
|
// Calculate next logical version based on base
|
|
const versionParts = baseVersion.split('.');
|
|
const major = parseInt(versionParts[0]) || 0;
|
|
const minor = parseInt(versionParts[1]) || 0;
|
|
const patch = parseInt(versionParts[2]) || 0;
|
|
|
|
// Increment minor version for development
|
|
const nextVersion = `${major}.${minor + 1}.0`;
|
|
currentVersion = `${nextVersion}-dev.${commits}+${shortHash}${dirtyFlag}`;
|
|
} else if (isDirty) {
|
|
// On a tag but with uncommitted changes
|
|
currentVersion = `${baseVersion}-dirty`;
|
|
} else {
|
|
// Exactly on a tag
|
|
currentVersion = baseVersion;
|
|
}
|
|
} else {
|
|
// Fallback if git describe doesn't match expected pattern
|
|
console.warn('[VersionUtils] Could not parse git describe output:', gitDescribe);
|
|
currentVersion = gitDescribe.replace(/^v/, '');
|
|
}
|
|
} else {
|
|
throw new Error('git describe returned empty');
|
|
}
|
|
} 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
|
|
}; |