diff --git a/.github/workflows/rc-release.yml b/.github/workflows/rc-release.yml index 9eb98ba1d..56c05569b 100644 --- a/.github/workflows/rc-release.yml +++ b/.github/workflows/rc-release.yml @@ -93,19 +93,105 @@ jobs: rcourtman/pulse:v${{ steps.check.outputs.base_version }}-rc${{ steps.check.outputs.rc_number }} rcourtman/pulse:rc + - name: Set up Node.js + if: steps.check.outputs.create_release == 'true' + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + + - name: Install dependencies and build CSS + if: steps.check.outputs.create_release == 'true' + run: | + echo "Installing dependencies for CSS build..." + npm install + echo "Building CSS with Tailwind..." + npm run build:css + - name: Create Release Archive if: steps.check.outputs.create_release == 'true' run: | - # Create release tarball - tar -czf pulse-v${{ steps.check.outputs.base_version }}-rc${{ steps.check.outputs.rc_number }}.tar.gz \ - --exclude=node_modules \ - --exclude=.git \ - --exclude=.env \ - --exclude=data \ - --exclude=*.log \ - --exclude=temp-release \ - server src scripts package.json package-lock.json README.md LICENSE CHANGELOG.md \ - docker-compose.yml Dockerfile + echo "📦 Building RC release tarball..." + + # Create staging directory for proper release structure + RC_VERSION="${{ steps.check.outputs.base_version }}-rc${{ steps.check.outputs.rc_number }}" + RELEASE_DIR_NAME="pulse-v$RC_VERSION" + STAGING_PARENT_DIR="pulse-rc-staging" + STAGING_FULL_PATH="$STAGING_PARENT_DIR/$RELEASE_DIR_NAME" + + # Cleanup and create staging + rm -rf "$STAGING_PARENT_DIR" + mkdir -p "$STAGING_FULL_PATH" + + echo "Copying application files to $STAGING_FULL_PATH..." + + # Copy server files (excluding tests) - using rsync for better handling + rsync -av --progress server/ "$STAGING_FULL_PATH/server/" --exclude 'tests/' || cp -r server "$STAGING_FULL_PATH/" + + # Copy source files (including built CSS and public assets) + mkdir -p "$STAGING_FULL_PATH/src" + rsync -av --progress src/public/ "$STAGING_FULL_PATH/src/public/" || cp -r src/public "$STAGING_FULL_PATH/src/" + cp src/index.css "$STAGING_FULL_PATH/src/" 2>/dev/null || echo "Warning: src/index.css not found" + cp src/tailwind.config.js "$STAGING_FULL_PATH/src/" 2>/dev/null || echo "Warning: src/tailwind.config.js not found" + cp src/postcss.config.js "$STAGING_FULL_PATH/src/" 2>/dev/null || echo "Warning: src/postcss.config.js not found" + + # Copy root files + cp package.json "$STAGING_FULL_PATH/" + cp package-lock.json "$STAGING_FULL_PATH/" + [ -f README.md ] && cp README.md "$STAGING_FULL_PATH/" + [ -f LICENSE ] && cp LICENSE "$STAGING_FULL_PATH/" + [ -f CHANGELOG.md ] && cp CHANGELOG.md "$STAGING_FULL_PATH/" + [ -f docker-compose.yml ] && cp docker-compose.yml "$STAGING_FULL_PATH/" + [ -f Dockerfile ] && cp Dockerfile "$STAGING_FULL_PATH/" + + # Copy scripts and docs + echo "Copying scripts directory..." + mkdir -p "$STAGING_FULL_PATH/scripts/" + if [ -f "scripts/install-pulse.sh" ]; then + cp scripts/install-pulse.sh "$STAGING_FULL_PATH/scripts/" + echo "✓ Copied install-pulse.sh" + else + echo "⚠️ Warning: scripts/install-pulse.sh not found" + fi + + if [ -d "docs" ]; then + rsync -av --progress docs/ "$STAGING_FULL_PATH/docs/" || cp -r docs "$STAGING_FULL_PATH/" + fi + + # Install production dependencies in staging + echo "Installing production dependencies..." + (cd "$STAGING_FULL_PATH" && npm install --omit=dev --ignore-scripts) + + # Verify essential files + echo "Verifying essential files..." + if [ ! -f "$STAGING_FULL_PATH/package.json" ]; then + echo "Error: Missing package.json" + exit 1 + fi + if [ ! -f "$STAGING_FULL_PATH/server/index.js" ]; then + echo "Error: Missing server/index.js" + exit 1 + fi + if [ ! -d "$STAGING_FULL_PATH/node_modules" ]; then + echo "Error: Missing node_modules" + exit 1 + fi + if [ ! -f "$STAGING_FULL_PATH/scripts/install-pulse.sh" ]; then + echo "Error: Missing scripts/install-pulse.sh" + exit 1 + fi + echo "✓ All essential files verified" + + # Create tarball with proper directory structure + echo "Creating tarball..." + (cd "$STAGING_PARENT_DIR" && tar -czf "../pulse-v$RC_VERSION.tar.gz" "$RELEASE_DIR_NAME") + + # Cleanup staging + rm -rf "$STAGING_PARENT_DIR" + + # Show tarball info + ls -lh "pulse-v$RC_VERSION.tar.gz" + echo "✅ RC release tarball created with production dependencies" - name: Create RC Release if: steps.check.outputs.create_release == 'true' diff --git a/server/index.js b/server/index.js index aa048b4f5..b370d1896 100644 --- a/server/index.js +++ b/server/index.js @@ -739,15 +739,25 @@ app.get('/api/version', async (req, res) => { let latestVersion = currentVersion; let updateAvailable = false; + let releaseUrl = null; - try { - // Try to check for updates, but don't fail if it doesn't work - const updateInfo = await updateManager.checkForUpdates(); - latestVersion = updateInfo.latestVersion || currentVersion; - updateAvailable = updateInfo.hasUpdate || false; - } catch (updateError) { - // Log the error but continue with current version info - console.error("[Version API] Error checking for updates:", updateError.message); + // Check if this is a development build (ahead of all releases) + const isDevelopmentBuild = currentVersion.includes('-dev.') || currentVersion.includes('-dirty'); + + if (!isDevelopmentBuild) { + try { + // Check for channel override from query parameter (for preview functionality) + const channelOverride = req.query.channel; + + // Try to check for updates with optional channel override + const updateInfo = await updateManager.checkForUpdates(channelOverride); + latestVersion = updateInfo.latestVersion || currentVersion; + updateAvailable = updateInfo.updateAvailable || false; + releaseUrl = updateInfo.releaseUrl; + } catch (updateError) { + // Log the error but continue with current version info + console.error("[Version API] Error checking for updates:", updateError.message); + } } res.json({ @@ -755,7 +765,8 @@ app.get('/api/version', async (req, res) => { latestVersion: latestVersion, updateAvailable: updateAvailable, gitBranch: gitBranch, - isDevelopment: isDevelopment + isDevelopment: isDevelopment, + releaseUrl: releaseUrl }); } catch (error) { console.error("[Version API] Error in version endpoint:", error); diff --git a/server/versionUtils.js b/server/versionUtils.js index e6730216c..b784a90f7 100644 --- a/server/versionUtils.js +++ b/server/versionUtils.js @@ -32,32 +32,48 @@ function getCurrentVersionInfo() { 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', { + // Use git describe for accurate development versioning + const gitDescribe = execSync('git describe --tags --dirty', { cwd: gitDir, - encoding: 'utf8', - shell: '/bin/bash' + encoding: 'utf8' }).trim(); - if (latestStableTag) { - // Remove 'v' prefix to get base version - const baseVersion = latestStableTag.replace(/^v/, ''); + if (gitDescribe) { + // Parse git describe output: v3.23.1-109-g3ee29b2-dirty + const match = gitDescribe.match(/^v([^-]+)(?:-(\d+)-g([a-f0-9]+))?(-dirty)?$/); - // 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}`; + 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 { - // No commits since stable, use base version - currentVersion = baseVersion; + // 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'); diff --git a/src/public/js/main.js b/src/public/js/main.js index 54fd13b15..b65d47a7d 100644 --- a/src/public/js/main.js +++ b/src/public/js/main.js @@ -160,14 +160,28 @@ document.addEventListener('DOMContentLoaded', function() { return allFound; } - function fetchVersion() { + async function fetchVersion() { const versionSpan = document.getElementById('app-version'); if (!versionSpan) { console.error('Version span element not found'); return; } - fetch('/api/version') + // Get user's channel preference from config + let userChannel = 'stable'; // default + try { + const configResponse = await fetch('/api/config'); + if (configResponse.ok) { + const config = await configResponse.json(); + userChannel = config.advanced?.updateChannel || 'stable'; + } + } catch (error) { + console.warn('Could not load user channel preference, using stable'); + } + + // Check version with user's preferred channel + const versionUrl = userChannel === 'stable' ? '/api/version' : `/api/version?channel=${userChannel}`; + fetch(versionUrl) .then(response => { if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); @@ -221,16 +235,24 @@ document.addEventListener('DOMContentLoaded', function() { updateIndicator.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); - window.open('https://github.com/rcourtman/Pulse/releases/latest', '_blank'); + const releaseUrl = data.releaseUrl || 'https://github.com/rcourtman/Pulse/releases/latest'; + window.open(releaseUrl, '_blank'); }); - // Insert after version link - versionSpan.parentNode.insertBefore(updateIndicator, versionSpan.nextSibling); + // Insert after version link with a space + const spacer = document.createTextNode(' '); + versionSpan.parentNode.insertBefore(spacer, versionSpan.nextSibling); + versionSpan.parentNode.insertBefore(updateIndicator, spacer.nextSibling); } } else { // Remove update indicator if no update available const existingIndicator = document.getElementById('update-indicator'); if (existingIndicator) { + // Also remove the spacer that comes before it + const spacer = existingIndicator.previousSibling; + if (spacer && spacer.nodeType === Node.TEXT_NODE && spacer.textContent === ' ') { + spacer.remove(); + } existingIndicator.remove(); } } diff --git a/src/public/js/ui/settings.js b/src/public/js/ui/settings.js index 9b7a01915..e50ccdc86 100644 --- a/src/public/js/ui/settings.js +++ b/src/public/js/ui/settings.js @@ -543,6 +543,22 @@ PulseApp.ui.settings = (() => {