Merge pull request #142 from rcourtman/develop

fix: add CSS build step to RC release workflow
This commit is contained in:
rcourtman
2025-06-13 23:11:11 +01:00
committed by GitHub
5 changed files with 369 additions and 53 deletions
+96 -10
View File
@@ -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'
+20 -9
View File
@@ -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);
+36 -20
View File
@@ -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');
+27 -5
View File
@@ -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();
}
}
+190 -9
View File
@@ -543,6 +543,22 @@ PulseApp.ui.settings = (() => {
<div id="update-channel-description" class="mt-2 text-xs text-gray-500 dark:text-gray-400">
<strong>Stable:</strong> Thoroughly tested releases for production use
</div>
<div class="mt-3 flex flex-wrap gap-2">
<button type="button" onclick="PulseApp.ui.settings.switchToChannelAndUpdate('stable')"
class="px-3 py-2 bg-green-600 hover:bg-green-700 text-white text-xs rounded-md flex items-center gap-1">
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
</svg>
Switch to Stable & Update
</button>
<button type="button" onclick="PulseApp.ui.settings.switchToChannelAndUpdate('rc')"
class="px-3 py-2 bg-blue-600 hover:bg-blue-700 text-white text-xs rounded-md flex items-center gap-1">
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/>
</svg>
Switch to RC & Update
</button>
</div>
<div id="rc-warning" class="mt-3 p-3 bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-lg hidden">
<div class="flex items-start gap-2">
<svg class="w-4 h-4 text-amber-600 dark:text-amber-400 mt-0.5 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
@@ -1348,6 +1364,8 @@ PulseApp.ui.settings = (() => {
const updateChannelInfoElement = document.getElementById('update-channel-info');
const channelMismatchWarning = document.getElementById('channel-mismatch-warning');
try {
if (!latestVersionElement) return;
try {
@@ -1373,13 +1391,38 @@ PulseApp.ui.settings = (() => {
} else {
// Use the server's update check API with optional channel override
const url = channelOverride ? `/api/updates/check?channel=${channelOverride}` : '/api/updates/check';
data = await PulseApp.apiClient.get(url);
// Cache the result
updateCache.set(cacheKey, {
data: data,
timestamp: Date.now()
});
try {
data = await PulseApp.apiClient.get(url);
// Cache the result
updateCache.set(cacheKey, {
data: data,
timestamp: Date.now()
});
} catch (error) {
if (error.message && (error.message.includes('rate limit') || error.message.includes('rateLimited'))) {
// Show user-friendly rate limit message
console.warn('[Settings] GitHub API rate limited, showing cached data if available');
showMessage('Update check temporarily unavailable due to GitHub rate limits. Please try again later.', 'warning');
// Use cached data if available, even if expired
if (cachedResult) {
console.log('[Settings] Using expired cache due to rate limiting');
data = cachedResult.data;
} else {
// No cached data available
const versionStatusElement = document.getElementById('version-status');
if (versionStatusElement) {
versionStatusElement.innerHTML = '<span class="text-amber-600 dark:text-amber-400">⚠️ Update check unavailable - rate limited</span>';
}
return;
}
} else {
// Re-throw other errors
throw error;
}
}
}
// Add preview indicator if using channel override
@@ -1531,6 +1574,17 @@ PulseApp.ui.settings = (() => {
}
}
}
} catch (error) {
console.error('[Settings] checkLatestVersion failed:', error);
if (versionStatusElement) {
if (error.message && (error.message.includes('rate limit') || error.message.includes('rateLimited'))) {
versionStatusElement.innerHTML = '<span class="text-amber-600 dark:text-amber-400">⚠️ Update check unavailable - rate limited</span>';
} else {
versionStatusElement.innerHTML = '<span class="text-red-500 dark:text-red-400">Update check failed</span>';
}
}
}
}
// Simple version comparison (assumes semver format)
@@ -1880,6 +1934,27 @@ PulseApp.ui.settings = (() => {
return;
}
// Check if the current channel setting matches the preview
const currentChannelSelect = document.querySelector('select[name="UPDATE_CHANNEL"]');
const selectedChannel = currentChannelSelect ? currentChannelSelect.value : 'stable';
// Save the current channel setting before applying update if it's different
const channelMismatchWarning = document.getElementById('channel-mismatch-warning');
if (channelMismatchWarning && !channelMismatchWarning.classList.contains('hidden')) {
// User is previewing a different channel, save settings first
showMessage('Saving channel preference before applying update...', 'info');
try {
await saveConfiguration();
// Wait a moment for settings to be saved
await new Promise(resolve => setTimeout(resolve, 500));
} catch (error) {
console.error('Failed to save channel setting:', error);
showMessage('Failed to save channel setting. Please save settings manually first.', 'error');
return;
}
}
// Find the tarball asset
const tarballAsset = latestReleaseData.assets.find(asset =>
asset.name.endsWith('.tar.gz') && asset.name.includes('pulse')
@@ -1904,9 +1979,13 @@ PulseApp.ui.settings = (() => {
return;
}
// Show channel information in confirmation
const isRCVersion = latestReleaseData.tag_name.toLowerCase().includes('-rc');
const channelInfo = isRCVersion ? ' (RC Channel)' : ' (Stable Channel)';
// Confirm update
PulseApp.ui.toast.confirm(
`Update to version ${latestReleaseData.tag_name}? The application will restart automatically after the update is applied.`,
`Update to version ${latestReleaseData.tag_name}${channelInfo}? The application will restart automatically after the update is applied.`,
async () => {
await _performUpdate(latestReleaseData, tarballAsset);
}
@@ -3227,6 +3306,25 @@ PulseApp.ui.settings = (() => {
URL.revokeObjectURL(url);
}
// Message display helper function
function showMessage(message, type = 'info') {
switch (type) {
case 'success':
PulseApp.ui.toast.success(message);
break;
case 'error':
PulseApp.ui.toast.error(message);
break;
case 'warning':
PulseApp.ui.toast.warning(message);
break;
case 'info':
default:
PulseApp.ui.toast.info(message);
break;
}
}
// Public API
// Handle update channel selection change
function onUpdateChannelChange(value) {
@@ -3239,13 +3337,40 @@ PulseApp.ui.settings = (() => {
}
}
// Show loading state during channel switch
const versionStatusElement = document.getElementById('version-status');
if (versionStatusElement) {
versionStatusElement.innerHTML = '<span class="text-gray-500 dark:text-gray-400">Checking for updates...</span>';
}
// Clear any existing update cache for the selected channel to ensure fresh data
const cacheKey = value || 'default';
updateCache.delete(cacheKey);
// Re-check for updates with the selected channel (preview mode)
// Debounce rapid changes to prevent API spam
if (updateCheckTimeout) {
clearTimeout(updateCheckTimeout);
}
updateCheckTimeout = setTimeout(() => {
checkLatestVersion(value);
updateCheckTimeout = setTimeout(async () => {
try {
await checkLatestVersion(value);
// Show message indicating this is a preview
if (versionStatusElement && !versionStatusElement.innerHTML.includes('Update available') && !versionStatusElement.innerHTML.includes('Up to date')) {
const channelName = value === 'rc' ? 'RC' : 'Stable';
versionStatusElement.innerHTML += `<br><small class="text-blue-600 dark:text-blue-400">Preview: ${channelName} channel (save settings to apply)</small>`;
}
} catch (error) {
console.error('[Settings] Channel switch failed:', error);
if (versionStatusElement) {
if (error.message && (error.message.includes('rate limit') || error.message.includes('rateLimited'))) {
versionStatusElement.innerHTML = '<span class="text-amber-600 dark:text-amber-400">⚠️ Update check unavailable - rate limited</span>';
} else {
versionStatusElement.innerHTML = '<span class="text-red-500 dark:text-red-400">Update check failed - please try again later</span>';
}
}
}
}, 300);
}
@@ -3276,6 +3401,42 @@ PulseApp.ui.settings = (() => {
}
}, 2000);
}
// Show reminder to save settings
showMessage(`Switched to ${targetChannel === 'rc' ? 'RC' : 'Stable'} channel. Remember to save settings to apply the change.`, 'info');
}
}
// Quick switch to channel and apply update if available
async function switchToChannelAndUpdate(targetChannel) {
try {
// Switch channel
const channelSelect = document.querySelector('select[name="UPDATE_CHANNEL"]');
if (channelSelect) {
channelSelect.value = targetChannel;
}
// Save settings immediately
showMessage('Switching channel and saving settings...', 'info');
await saveConfiguration();
// Clear cache and check for updates
updateCache.clear();
await checkLatestVersion();
// If update is available, offer to apply it immediately
const versionStatusElement = document.getElementById('version-status');
if (versionStatusElement && versionStatusElement.innerHTML.includes('Update available')) {
const channelName = targetChannel === 'rc' ? 'RC' : 'Stable';
showMessage(`Switched to ${channelName} channel successfully! An update is available.`, 'success');
} else {
const channelName = targetChannel === 'rc' ? 'RC' : 'Stable';
showMessage(`Switched to ${channelName} channel successfully!`, 'success');
}
} catch (error) {
console.error('Error switching channel:', error);
showMessage(`Failed to switch to ${targetChannel} channel: ${error.message}`, 'error');
}
}
@@ -3769,6 +3930,25 @@ PulseApp.ui.settings = (() => {
}, 3000);
}
function showMessage(message, type = 'info') {
// Use the proper toast system based on message type
switch (type) {
case 'success':
PulseApp.ui.toast.success(message);
break;
case 'error':
PulseApp.ui.toast.error(message);
break;
case 'warning':
PulseApp.ui.toast.warning(message);
break;
case 'info':
default:
PulseApp.ui.toast.info(message);
break;
}
}
function renderAlertManagementTab() {
return `
<div class="space-y-6">
@@ -3894,6 +4074,7 @@ PulseApp.ui.settings = (() => {
onUpdateChannelChange,
switchToRecommendedChannel,
switchToChannel,
switchToChannelAndUpdate,
acknowledgeStableChoice,
proceedWithStableSwitch,
clearUpdateCache,