617e778a48
Add dual-channel release strategy for stable and beta releases: Release Channels: - Stable channel: production-ready releases (stable, latest, v2.3.0) - Beta channel: early access features (beta, v2.3.0-beta.1) - Configurable via PICPEAK_CHANNEL environment variable Update Notifications: - Admin dashboard shows available updates for configured channel - Checks GitHub Releases API with 1-hour cache - Can be disabled with UPDATE_CHECK_ENABLED=false CI/CD Changes: - New release-please-beta.yml workflow for beta prereleases - Docker build workflow produces stable/beta tags based on branch - Beta versions use v2.3.0-beta.1 format New Files: - .github/workflows/release-please-beta.yml - release-please-config-beta.json - .release-please-manifest-beta.json - backend/src/services/updateCheckService.js - frontend/src/components/admin/UpdateNotification.tsx Modified Files: - docker-compose.production.yml (channel selection) - .env.example (PICPEAK_CHANNEL, UPDATE_CHECK_ENABLED) - backend/src/routes/adminSystem.js (/updates endpoint) - frontend components (VersionInfo, AdminDashboard) - i18n locales (en.json, de.json) - README.md and DEPLOYMENT_GUIDE.md (documentation)
213 lines
5.5 KiB
JavaScript
213 lines
5.5 KiB
JavaScript
const axios = require('axios');
|
|
const fs = require('fs').promises;
|
|
const path = require('path');
|
|
const logger = require('../utils/logger');
|
|
|
|
// Cache for version info (avoid hitting GitHub API too often)
|
|
let versionCache = null;
|
|
let lastCheck = 0;
|
|
const CACHE_TTL = 60 * 60 * 1000; // 1 hour cache
|
|
|
|
/**
|
|
* Get current installed version from package.json
|
|
*/
|
|
async function getCurrentVersion() {
|
|
try {
|
|
const packagePath = path.join(__dirname, '../../package.json');
|
|
const packageContent = await fs.readFile(packagePath, 'utf8');
|
|
const packageJson = JSON.parse(packageContent);
|
|
return packageJson.version || '0.0.0';
|
|
} catch (err) {
|
|
logger.error('Could not read package.json for version:', err);
|
|
return '0.0.0';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Determine current release channel from version or environment
|
|
*/
|
|
function getCurrentChannel(version) {
|
|
// Check environment variable first
|
|
const envChannel = process.env.PICPEAK_RELEASE_CHANNEL;
|
|
if (envChannel && ['stable', 'beta'].includes(envChannel)) {
|
|
return envChannel;
|
|
}
|
|
|
|
// Infer from version string
|
|
if (version && version.includes('-beta')) {
|
|
return 'beta';
|
|
}
|
|
return 'stable';
|
|
}
|
|
|
|
/**
|
|
* Parse version string into comparable parts
|
|
*/
|
|
function parseVersion(version) {
|
|
if (!version) return null;
|
|
|
|
// Handle versions like "2.3.0" or "2.3.0-beta.1"
|
|
const match = version.match(/^(\d+)\.(\d+)\.(\d+)(?:-beta\.(\d+))?$/);
|
|
if (!match) return null;
|
|
|
|
return {
|
|
major: parseInt(match[1], 10),
|
|
minor: parseInt(match[2], 10),
|
|
patch: parseInt(match[3], 10),
|
|
beta: match[4] ? parseInt(match[4], 10) : null,
|
|
isBeta: !!match[4]
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Compare two versions
|
|
* Returns: 1 if a > b, -1 if a < b, 0 if equal
|
|
*/
|
|
function compareVersions(a, b) {
|
|
const va = parseVersion(a);
|
|
const vb = parseVersion(b);
|
|
|
|
if (!va || !vb) return 0;
|
|
|
|
// Compare major.minor.patch
|
|
if (va.major !== vb.major) return va.major > vb.major ? 1 : -1;
|
|
if (va.minor !== vb.minor) return va.minor > vb.minor ? 1 : -1;
|
|
if (va.patch !== vb.patch) return va.patch > vb.patch ? 1 : -1;
|
|
|
|
// Handle beta vs stable
|
|
if (va.isBeta && !vb.isBeta) return -1; // beta < stable
|
|
if (!va.isBeta && vb.isBeta) return 1; // stable > beta
|
|
|
|
// Both are beta - compare beta numbers
|
|
if (va.isBeta && vb.isBeta) {
|
|
if (va.beta !== vb.beta) return va.beta > vb.beta ? 1 : -1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Fetch available versions from GitHub Releases
|
|
* Uses GitHub Releases API which is publicly accessible without authentication
|
|
*/
|
|
async function fetchAvailableVersions() {
|
|
try {
|
|
// Use GitHub Releases API (public, no auth required)
|
|
const response = await axios.get(
|
|
'https://api.github.com/repos/the-luap/picpeak/releases',
|
|
{
|
|
headers: {
|
|
'Accept': 'application/vnd.github+json',
|
|
'X-GitHub-Api-Version': '2022-11-28',
|
|
'User-Agent': 'PicPeak-Update-Checker'
|
|
},
|
|
timeout: 10000
|
|
}
|
|
);
|
|
|
|
// Extract version tags from releases
|
|
const versions = {
|
|
stable: [],
|
|
beta: []
|
|
};
|
|
|
|
for (const release of response.data) {
|
|
const tag = release.tag_name;
|
|
if (!tag) continue;
|
|
|
|
// Remove 'v' prefix if present
|
|
const version = tag.startsWith('v') ? tag.substring(1) : tag;
|
|
|
|
if (version.match(/^\d+\.\d+\.\d+$/)) {
|
|
// Stable version
|
|
versions.stable.push(version);
|
|
} else if (version.match(/^\d+\.\d+\.\d+-beta\.\d+$/)) {
|
|
// Beta version
|
|
versions.beta.push(version);
|
|
}
|
|
}
|
|
|
|
// Sort versions descending (newest first)
|
|
versions.stable.sort((a, b) => compareVersions(b, a));
|
|
versions.beta.sort((a, b) => compareVersions(b, a));
|
|
|
|
return versions;
|
|
} catch (error) {
|
|
logger.error('Failed to fetch available versions from GitHub:', error.message);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check for available updates
|
|
*/
|
|
async function checkForUpdates(forceRefresh = false) {
|
|
const now = Date.now();
|
|
|
|
// Use cache if available and not expired
|
|
if (!forceRefresh && versionCache && (now - lastCheck) < CACHE_TTL) {
|
|
return versionCache;
|
|
}
|
|
|
|
const currentVersion = await getCurrentVersion();
|
|
const currentChannel = getCurrentChannel(currentVersion);
|
|
const availableVersions = await fetchAvailableVersions();
|
|
|
|
if (!availableVersions) {
|
|
return {
|
|
current: currentVersion,
|
|
channel: currentChannel,
|
|
updateAvailable: false,
|
|
error: 'Unable to check for updates'
|
|
};
|
|
}
|
|
|
|
// Determine latest version for current channel
|
|
const latestStable = availableVersions.stable[0] || currentVersion;
|
|
const latestBeta = availableVersions.beta[0] || currentVersion;
|
|
const latestForChannel = currentChannel === 'beta' ? latestBeta : latestStable;
|
|
|
|
const updateAvailable = compareVersions(latestForChannel, currentVersion) > 0;
|
|
|
|
// Also check if there's a newer beta for stable users who want to preview
|
|
const newerBetaAvailable = currentChannel === 'stable' &&
|
|
availableVersions.beta.length > 0 &&
|
|
compareVersions(latestBeta, currentVersion) > 0;
|
|
|
|
const result = {
|
|
current: currentVersion,
|
|
channel: currentChannel,
|
|
latest: {
|
|
stable: latestStable,
|
|
beta: latestBeta,
|
|
forChannel: latestForChannel
|
|
},
|
|
updateAvailable,
|
|
newerBetaAvailable,
|
|
lastChecked: new Date().toISOString()
|
|
};
|
|
|
|
// Update cache
|
|
versionCache = result;
|
|
lastCheck = now;
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Clear the version cache (useful for testing)
|
|
*/
|
|
function clearCache() {
|
|
versionCache = null;
|
|
lastCheck = 0;
|
|
}
|
|
|
|
module.exports = {
|
|
checkForUpdates,
|
|
getCurrentVersion,
|
|
getCurrentChannel,
|
|
compareVersions,
|
|
parseVersion,
|
|
clearCache
|
|
};
|