mirror of
https://github.com/lobehub/lobehub.git
synced 2026-03-26 13:19:34 +07:00
✨ feat(desktop): unified update channel switching with S3 distribution (#12644)
* ✨ feat(desktop): add update channel settings for desktop app * 🔧 chore(desktop): update test scripts for multi-channel update flow - Support stable/nightly/canary channel structure in generate-manifest.sh - Add --all-channels flag for generating manifests across all channels - Dual-mode run-test.sh: packaged (full updater) and --dev (UI only) - Fix package:mac:local to skip signing for local builds - Document Squirrel.Mac signature validation limitation * 🔧 chore(desktop): update local app update configuration - Change provider from GitHub to Generic for local testing. - Update local server URL and cache directory settings. - Revise comments for clarity on usage and configuration. Signed-off-by: Innei <tukon479@gmail.com> * 🐛 fix(desktop): fix update channel switch race condition and downgrade flag - P1: Use generation counter to discard stale check results when channel is switched mid-flight. Pending recheck is scheduled after current check completes instead of forcing concurrent checks. - P2: Explicitly reset allowDowngrade=false on non-downgrade transitions to prevent stale downgrade permission from persisting. - Fix GitHub fallback repo name (lobe-chat -> lobehub). * 🔧 chore(settings): remove dynamic import for Beta component from componentMap - Eliminated the dynamic import for the Beta settings tab, streamlining the component map. Signed-off-by: Innei <tukon479@gmail.com> * 🔧 chore(settings): simplify UpdateChannel component structure - Refactored the UpdateChannel component to streamline the Select component usage by removing unnecessary nested children. Signed-off-by: Innei <tukon479@gmail.com> * update * 🐛 fix(desktop): strip channel suffix from UPDATE_SERVER_URL before appending channel The UPDATE_SERVER_URL secret may already contain a channel path (e.g., /stable). Previously, the code unconditionally appended /{channel}, resulting in double paths like /stable/stable/stable-mac.yml. Now both electron-builder.mjs and UpdaterManager strip any trailing channel suffix before re-appending the correct channel, supporting both legacy URLs (with channel) and clean base URLs. * update * update * redesign ui - Added `getUpdaterState` method to `UpdaterManager` for retrieving current update status. - Introduced `UpdaterState` type to encapsulate update progress, stage, and error messages. - Updated UI components to reflect update states, including checking, downloading, and latest version notifications. - Enhanced menu items for macOS and Windows to display appropriate update statuses. - Localized new update messages in English and Chinese. This improves user experience by providing real-time feedback during the update process. Signed-off-by: Innei <tukon479@gmail.com> * Enhance UpdaterManager tests and mock implementations - Updated tests for UpdaterManager to reflect changes in broadcasting update states, including 'checking', 'downloading', and 'error' stages. - Modified mock implementations in macOS and Windows test files to include `getUpdaterState` and `installNow` methods for better state management. - Improved test coverage for update availability and download processes. These changes ensure more accurate testing of the update flow and enhance the overall reliability of the UpdaterManager functionality. Signed-off-by: Innei <tukon479@gmail.com> --------- Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
@@ -3,15 +3,14 @@ import path from 'node:path';
|
||||
|
||||
import YAML from 'yaml';
|
||||
|
||||
// 配置
|
||||
// Support both stable-mac.yml (stable channel) and latest-mac.yml (fallback)
|
||||
const STABLE_outputFileName = 'stable-mac.yml';
|
||||
const LATEST_outputFileName = 'latest-mac.yml';
|
||||
const RELEASE_DIR = path.resolve('release');
|
||||
|
||||
// All channel manifest prefixes to process
|
||||
const CHANNEL_PREFIXES = ['stable', 'nightly', 'canary', 'latest'];
|
||||
|
||||
/**
|
||||
* 检测 latest-mac.yml 文件的平台类型
|
||||
* @param {Object} yamlContent - YAML 文件内容
|
||||
* Detect platform type from YAML content
|
||||
* @param {Object} yamlContent
|
||||
* @returns {'x64' | 'arm64' | 'both' | 'none'}
|
||||
*/
|
||||
function detectPlatform(yamlContent) {
|
||||
@@ -25,13 +24,12 @@ function detectPlatform(yamlContent) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 合并两个 latest-mac.yml 文件
|
||||
* @param {Object} x64Content - x64 平台的 YAML 内容
|
||||
* @param {Object} arm64Content - ARM64 平台的 YAML 内容
|
||||
* @returns {string} 合并后的 YAML 字符串
|
||||
* Merge x64 and ARM64 YAML files
|
||||
* @param {Object} x64Content
|
||||
* @param {Object} arm64Content
|
||||
* @returns {string}
|
||||
*/
|
||||
function mergeYamlFiles(x64Content, arm64Content) {
|
||||
// 以 ARM64 为基础(Apple Silicon 优先)
|
||||
const merged = {
|
||||
...arm64Content,
|
||||
files: [...arm64Content.files, ...x64Content.files],
|
||||
@@ -40,11 +38,6 @@ function mergeYamlFiles(x64Content, arm64Content) {
|
||||
return YAML.stringify(merged);
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取本地文件
|
||||
* @param {string} filePath - 文件路径
|
||||
* @returns {string | null} 文件内容或 null
|
||||
*/
|
||||
function readLocalFile(filePath) {
|
||||
try {
|
||||
if (fs.existsSync(filePath)) {
|
||||
@@ -60,11 +53,6 @@ function readLocalFile(filePath) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入本地文件
|
||||
* @param {string} filePath - 文件路径
|
||||
* @param {string} content - 文件内容
|
||||
*/
|
||||
function writeLocalFile(filePath, content) {
|
||||
try {
|
||||
fs.writeFileSync(filePath, content, 'utf8');
|
||||
@@ -76,118 +64,107 @@ function writeLocalFile(filePath, content) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 主函数
|
||||
* Merge mac YAML files for a given channel prefix
|
||||
* @param {string} prefix - e.g. 'stable', 'nightly', 'canary', 'latest'
|
||||
* @param {string[]} releaseFiles - files in release directory
|
||||
*/
|
||||
function mergeForPrefix(prefix, releaseFiles) {
|
||||
const outputFileName = `${prefix}-mac.yml`;
|
||||
const macYmlFiles = releaseFiles.filter(
|
||||
(f) => f.startsWith(`${prefix}-mac`) && f.endsWith('.yml'),
|
||||
);
|
||||
|
||||
if (macYmlFiles.length === 0) {
|
||||
console.log(`⚠️ No ${prefix}-mac*.yml files found, skipping`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`\n🔍 Processing ${prefix} channel: ${macYmlFiles.join(', ')} -> ${outputFileName}`);
|
||||
|
||||
const macFiles = [];
|
||||
|
||||
for (const fileName of macYmlFiles) {
|
||||
const filePath = path.join(RELEASE_DIR, fileName);
|
||||
const content = readLocalFile(filePath);
|
||||
|
||||
if (!content) continue;
|
||||
|
||||
try {
|
||||
const yamlContent = YAML.parse(content);
|
||||
const platform = detectPlatform(yamlContent);
|
||||
|
||||
if (platform === 'x64' || platform === 'arm64') {
|
||||
macFiles.push({ content, filename: fileName, platform, yaml: yamlContent });
|
||||
console.log(`🔍 Detected ${platform} platform in ${fileName}`);
|
||||
} else if (platform === 'both') {
|
||||
console.log(`✅ Found already merged file: ${fileName}`);
|
||||
writeLocalFile(path.join(RELEASE_DIR, outputFileName), content);
|
||||
return;
|
||||
} else {
|
||||
console.log(`⚠️ Unknown platform type: ${platform} in ${fileName}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`⚠️ Failed to parse ${fileName}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
const x64Files = macFiles.filter((f) => f.platform === 'x64');
|
||||
const arm64Files = macFiles.filter((f) => f.platform === 'arm64');
|
||||
|
||||
if (x64Files.length === 0 && arm64Files.length === 0) {
|
||||
console.log(`⚠️ No valid platform files found for ${prefix}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (x64Files.length === 0) {
|
||||
console.log(`⚠️ No x64 files found for ${prefix}, using ARM64 only`);
|
||||
writeLocalFile(path.join(RELEASE_DIR, outputFileName), arm64Files[0].content);
|
||||
return;
|
||||
}
|
||||
|
||||
if (arm64Files.length === 0) {
|
||||
console.log(`⚠️ No ARM64 files found for ${prefix}, using x64 only`);
|
||||
writeLocalFile(path.join(RELEASE_DIR, outputFileName), x64Files[0].content);
|
||||
return;
|
||||
}
|
||||
|
||||
const x64File = x64Files[0];
|
||||
const arm64File = arm64Files[0];
|
||||
|
||||
console.log(`🔄 Merging ${x64File.filename} (x64) and ${arm64File.filename} (ARM64)...`);
|
||||
const mergedContent = mergeYamlFiles(x64File.yaml, arm64File.yaml);
|
||||
|
||||
const mergedFilePath = path.join(RELEASE_DIR, outputFileName);
|
||||
writeLocalFile(mergedFilePath, mergedContent);
|
||||
|
||||
const mergedYaml = YAML.parse(mergedContent);
|
||||
const finalPlatform = detectPlatform(mergedYaml);
|
||||
|
||||
if (finalPlatform === 'both') {
|
||||
console.log(`✅ Successfully merged both x64 and ARM64 platforms for ${prefix}`);
|
||||
console.log(`📊 Final file contains ${mergedYaml.files.length} files`);
|
||||
} else {
|
||||
console.warn(`⚠️ Merge result unexpected: ${finalPlatform}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
console.log('🚀 Starting macOS Release file merge');
|
||||
console.log(`📁 Working directory: ${RELEASE_DIR}`);
|
||||
|
||||
// 1. 检查 release 目录下的所有文件
|
||||
const releaseFiles = fs.readdirSync(RELEASE_DIR);
|
||||
console.log(`📂 Files in release directory: ${releaseFiles.join(', ')}`);
|
||||
|
||||
// 2. 查找所有 stable-mac*.yml 和 latest-mac*.yml 文件
|
||||
// Prioritize stable-mac*.yml, fallback to latest-mac*.yml
|
||||
const stableMacYmlFiles = releaseFiles.filter(
|
||||
(f) => f.startsWith('stable-mac') && f.endsWith('.yml'),
|
||||
);
|
||||
const latestMacYmlFiles = releaseFiles.filter(
|
||||
(f) => f.startsWith('latest-mac') && f.endsWith('.yml'),
|
||||
);
|
||||
|
||||
// Use stable files if available, otherwise use latest
|
||||
const macYmlFiles = stableMacYmlFiles.length > 0 ? stableMacYmlFiles : latestMacYmlFiles;
|
||||
const outputFileName =
|
||||
stableMacYmlFiles.length > 0 ? STABLE_outputFileName : LATEST_outputFileName;
|
||||
|
||||
console.log(`🔍 Found stable macOS YAML files: ${stableMacYmlFiles.join(', ') || 'none'}`);
|
||||
console.log(`🔍 Found latest macOS YAML files: ${latestMacYmlFiles.join(', ') || 'none'}`);
|
||||
console.log(`🔍 Using files: ${macYmlFiles.join(', ')} -> ${outputFileName}`);
|
||||
|
||||
if (macYmlFiles.length === 0) {
|
||||
console.log('⚠️ No macOS YAML files found, skipping merge');
|
||||
return;
|
||||
for (const prefix of CHANNEL_PREFIXES) {
|
||||
mergeForPrefix(prefix, releaseFiles);
|
||||
}
|
||||
|
||||
// 3. 处理找到的文件,识别平台
|
||||
const macFiles = [];
|
||||
|
||||
for (const fileName of macYmlFiles) {
|
||||
const filePath = path.join(RELEASE_DIR, fileName);
|
||||
const content = readLocalFile(filePath);
|
||||
|
||||
if (!content) continue;
|
||||
|
||||
try {
|
||||
const yamlContent = YAML.parse(content);
|
||||
const platform = detectPlatform(yamlContent);
|
||||
|
||||
if (platform === 'x64' || platform === 'arm64') {
|
||||
macFiles.push({ content, filename: fileName, platform, yaml: yamlContent });
|
||||
console.log(`🔍 Detected ${platform} platform in ${fileName}`);
|
||||
} else if (platform === 'both') {
|
||||
console.log(`✅ Found already merged file: ${fileName}`);
|
||||
// 如果已经是合并后的文件,直接复制为最终文件
|
||||
writeLocalFile(path.join(RELEASE_DIR, outputFileName), content);
|
||||
return;
|
||||
} else {
|
||||
console.log(`⚠️ Unknown platform type: ${platform} in ${fileName}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`⚠️ Failed to parse ${fileName}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 检查是否有两个不同平台的文件
|
||||
const x64Files = macFiles.filter((f) => f.platform === 'x64');
|
||||
const arm64Files = macFiles.filter((f) => f.platform === 'arm64');
|
||||
|
||||
if (x64Files.length === 0 && arm64Files.length === 0) {
|
||||
console.log('⚠️ No valid platform files found');
|
||||
return;
|
||||
}
|
||||
|
||||
if (x64Files.length === 0) {
|
||||
console.log('⚠️ No x64 files found, using ARM64 only');
|
||||
writeLocalFile(path.join(RELEASE_DIR, outputFileName), arm64Files[0].content);
|
||||
return;
|
||||
}
|
||||
|
||||
if (arm64Files.length === 0) {
|
||||
console.log('⚠️ No ARM64 files found, using x64 only');
|
||||
writeLocalFile(path.join(RELEASE_DIR, outputFileName), x64Files[0].content);
|
||||
return;
|
||||
}
|
||||
|
||||
// 5. 合并 x64 和 ARM64 文件
|
||||
const x64File = x64Files[0];
|
||||
const arm64File = arm64Files[0];
|
||||
|
||||
console.log(`🔄 Merging ${x64File.filename} (x64) and ${arm64File.filename} (ARM64)...`);
|
||||
const mergedContent = mergeYamlFiles(x64File.yaml, arm64File.yaml);
|
||||
|
||||
// 6. 保存合并后的文件
|
||||
const mergedFilePath = path.join(RELEASE_DIR, outputFileName);
|
||||
writeLocalFile(mergedFilePath, mergedContent);
|
||||
|
||||
// 7. 验证合并结果
|
||||
const mergedYaml = YAML.parse(mergedContent);
|
||||
const finalPlatform = detectPlatform(mergedYaml);
|
||||
|
||||
if (finalPlatform === 'both') {
|
||||
console.log('✅ Successfully merged both x64 and ARM64 platforms');
|
||||
console.log(`📊 Final file contains ${mergedYaml.files.length} files`);
|
||||
} else {
|
||||
console.warn(`⚠️ Merge result unexpected: ${finalPlatform}`);
|
||||
}
|
||||
|
||||
console.log('🎉 Merge complete!');
|
||||
console.log('\n🎉 Merge complete!');
|
||||
} catch (error) {
|
||||
console.error('❌ Error during merge:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// 运行主函数
|
||||
await main();
|
||||
|
||||
Reference in New Issue
Block a user