Feat/automatic update installation (#5345)

Signed-off-by: AdemolaAri <ademola.ari@gmail.com>
This commit is contained in:
Ademola Arigbabuwo
2025-12-01 14:50:41 -05:00
committed by GitHub
parent 8e9b6c9bc6
commit c3c71a2ff4
5 changed files with 224 additions and 66 deletions
+108 -19
View File
@@ -9,6 +9,7 @@ import {
dialog,
Menu,
MenuItemConstructorOptions,
Notification,
} from 'electron';
import * as path from 'path';
import * as fs from 'fs/promises';
@@ -30,6 +31,9 @@ let githubUpdateInfo: {
// Store update state
let lastUpdateState: { updateAvailable: boolean; latestVersion?: string } | null = null;
// Track last reported progress to prevent backward jumps
let lastReportedProgress = 0;
// Track if IPC handlers have been registered
let ipcUpdateHandlersRegistered = false;
@@ -47,9 +51,10 @@ export function registerUpdateIpcHandlers() {
try {
log.info('Manual check for updates requested');
// Reset fallback flag
// Reset state for new update check
isUsingGitHubFallback = false;
githubUpdateInfo = {};
lastReportedProgress = 0; // Reset progress tracking
// Ensure auto-updater is properly initialized
if (!autoUpdater.currentVersion) {
@@ -116,6 +121,10 @@ export function registerUpdateIpcHandlers() {
lastUpdateState = { updateAvailable: true, latestVersion: result.latestVersion };
updateTrayIcon(true);
sendStatusToWindow('update-available', { version: result.latestVersion });
// Auto-download for GitHub fallback (matching autoDownload behavior)
log.info('Auto-downloading update via GitHub fallback...');
await githubAutoDownload(result.downloadUrl!, result.latestVersion!, 'manual check');
} else {
updateAvailable = false;
lastUpdateState = { updateAvailable: false };
@@ -149,12 +158,17 @@ export function registerUpdateIpcHandlers() {
try {
if (isUsingGitHubFallback && githubUpdateInfo.downloadUrl && githubUpdateInfo.latestVersion) {
log.info('Using GitHub fallback for download...');
lastReportedProgress = 0; // Reset progress tracking
const result = await githubUpdater.downloadUpdate(
githubUpdateInfo.downloadUrl,
githubUpdateInfo.latestVersion,
(percent) => {
sendStatusToWindow('download-progress', { percent });
// Only send if progress increased (monotonic)
if (percent > lastReportedProgress) {
lastReportedProgress = percent;
sendStatusToWindow('download-progress', { percent });
}
}
);
@@ -200,25 +214,26 @@ export function registerUpdateIpcHandlers() {
throw new Error('Update file not found. Please download the update first.');
}
// Show dialog to inform user about manual installation
// Improved dialog with clearer instructions
const dialogResult = (await dialog.showMessageBox({
type: 'info',
title: 'Update Downloaded',
message: 'The update has been downloaded to your Downloads folder.',
detail: `Please extract the zip file and move the Goose app to your Applications folder to complete the update.`,
buttons: ['Open Downloads', 'Cancel'],
title: 'Update Ready to Install',
message: `Version ${githubUpdateInfo.latestVersion} is ready to install.`,
detail: `The update has been downloaded and extracted. To complete the installation:\n\n1. Click "Open Folder" to view the new Goose.app\n2. Quit Goose (this app will close)\n3. Drag the new Goose.app to your Applications folder\n4. Replace the existing app when prompted\n\nThe update will be available the next time you launch Goose.`,
buttons: ['Open Folder & Quit', 'Open Folder Only', 'Cancel'],
defaultId: 0,
cancelId: 1,
cancelId: 2,
})) as unknown as { response: number };
if (dialogResult.response === 0) {
// Open the extracted folder or show the zip file
// Open folder and quit app for easy replacement
shell.showItemInFolder(updatePath);
// Optionally quit the app so user can replace it
setTimeout(() => {
app.quit();
}, 1000);
}, 1500); // Give user time to see the folder open
} else if (dialogResult.response === 1) {
// Just open folder, don't quit
shell.showItemInFolder(updatePath);
}
} catch (error) {
log.error('Error installing GitHub update:', error);
@@ -237,6 +252,10 @@ export function registerUpdateIpcHandlers() {
ipcMain.handle('get-update-state', () => {
return lastUpdateState;
});
ipcMain.handle('is-using-github-fallback', () => {
return isUsingGitHubFallback;
});
}
// Configure auto-updater
@@ -273,7 +292,7 @@ export function setupAutoUpdater(tray?: Tray) {
}
// Configure auto-updater settings
autoUpdater.autoDownload = false; // We'll trigger downloads manually
autoUpdater.autoDownload = true; // Automatically download updates when available
autoUpdater.autoInstallOnAppQuit = true;
// Enable updates in development mode for testing
@@ -330,7 +349,7 @@ export function setupAutoUpdater(tray?: Tray) {
githubUpdater
.checkForUpdates()
.then((result) => {
.then(async (result) => {
if (result.error) {
sendStatusToWindow('error', result.error);
} else if (result.updateAvailable) {
@@ -345,6 +364,10 @@ export function setupAutoUpdater(tray?: Tray) {
lastUpdateState = { updateAvailable: true, latestVersion: result.latestVersion };
updateTrayIcon(true);
sendStatusToWindow('update-available', { version: result.latestVersion });
// Auto-download for GitHub fallback (matching autoDownload behavior)
log.info('Auto-downloading update via GitHub fallback on startup...');
await githubAutoDownload(result.downloadUrl!, result.latestVersion!, 'on startup');
} else {
updateAvailable = false;
lastUpdateState = { updateAvailable: false };
@@ -365,6 +388,7 @@ export function setupAutoUpdater(tray?: Tray) {
autoUpdater.on('checking-for-update', () => {
log.info('Auto-updater: Checking for update...');
log.info(`Auto-updater: Feed URL during check: ${autoUpdater.getFeedURL()}`);
lastReportedProgress = 0; // Reset progress tracking for new check
sendStatusToWindow('checking-for-update');
});
@@ -421,6 +445,10 @@ export function setupAutoUpdater(tray?: Tray) {
updateAvailable = true;
updateTrayIcon(true);
sendStatusToWindow('update-available', { version: result.latestVersion });
// Auto-download for GitHub fallback (matching autoDownload behavior)
log.info('Auto-downloading update via GitHub fallback after error...');
await githubAutoDownload(result.downloadUrl!, result.latestVersion!, 'after error');
} else {
updateAvailable = false;
updateTrayIcon(false);
@@ -441,16 +469,37 @@ export function setupAutoUpdater(tray?: Tray) {
});
autoUpdater.on('download-progress', (progressObj) => {
let log_message = 'Download speed: ' + progressObj.bytesPerSecond;
log_message = log_message + ' - Downloaded ' + progressObj.percent + '%';
log_message = log_message + ' (' + progressObj.transferred + '/' + progressObj.total + ')';
log.info(log_message);
sendStatusToWindow('download-progress', progressObj);
const roundedPercent = Math.round(progressObj.percent);
// Only send progress if it increased (prevents backward jumps)
if (roundedPercent > lastReportedProgress) {
lastReportedProgress = roundedPercent;
const log_message = `Download: ${roundedPercent}% (${progressObj.transferred}/${progressObj.total}) @ ${Math.round(progressObj.bytesPerSecond / 1024)} KB/s`;
log.info(log_message);
sendStatusToWindow('download-progress', {
...progressObj,
percent: roundedPercent,
});
}
});
autoUpdater.on('update-downloaded', (info: UpdateInfo) => {
log.info('Update downloaded:', info);
sendStatusToWindow('update-downloaded', info);
// Show native notification
const notification = new Notification({
title: 'Update Ready',
body: `Version ${info.version} will be installed when you quit Goose. Click to install now.`,
});
notification.show();
// Optional: Add click handler to install immediately
notification.on('click', () => {
autoUpdater.quitAndInstall(false, true);
});
});
}
@@ -466,6 +515,46 @@ function sendStatusToWindow(event: string, data?: unknown) {
});
}
// centralize GitHub fallback auto-download logic.
async function githubAutoDownload(
downloadUrl: string,
latestVersion: string,
contextLabel = ''
): Promise<void> {
// Reset progress tracking for new download
lastReportedProgress = 0;
try {
const downloadResult = await githubUpdater.downloadUpdate(
downloadUrl,
latestVersion,
(percent) => {
// Only send if progress increased (monotonic)
if (percent > lastReportedProgress) {
lastReportedProgress = percent;
sendStatusToWindow('download-progress', { percent });
}
}
);
if (downloadResult.success && downloadResult.downloadPath) {
githubUpdateInfo.downloadPath = downloadResult.downloadPath;
githubUpdateInfo.extractedPath = downloadResult.extractedPath;
sendStatusToWindow('update-downloaded', { version: latestVersion });
} else {
log.error(
`GitHub auto-download failed${contextLabel ? ` (${contextLabel})` : ''}:`,
downloadResult.error
);
}
} catch (downloadError) {
log.error(
`Error during GitHub auto-download${contextLabel ? ` (${contextLabel})` : ''}:`,
downloadError
);
}
}
function updateTrayIcon(hasUpdate: boolean) {
if (!trayRef) return;
+15 -3
View File
@@ -106,7 +106,7 @@ export class GitHubUpdater {
log.info(`GitHubUpdater: Looking for asset named: ${assetName}`);
log.info(`GitHubUpdater: Available assets: ${release.assets.map((a) => a.name).join(', ')}`);
const asset = release.assets.find((a) => a.name === assetName);
const asset = release.assets.find((a) => a.name.toLowerCase() === assetName.toLowerCase()); // keeping comparison to lower case becasue Goose vs goose
if (asset) {
downloadUrl = asset.browser_download_url;
log.info(`GitHubUpdater: Found matching asset: ${asset.name} (${asset.size} bytes)`);
@@ -115,6 +115,12 @@ export class GitHubUpdater {
log.warn(`GitHubUpdater: No matching asset found for ${assetName}`);
}
if (!downloadUrl) {
throw new Error(
`Update Available but no download URL found for platform: ${platform}, arch: ${arch}`
);
}
return {
updateAvailable: true,
latestVersion,
@@ -159,6 +165,7 @@ export class GitHubUpdater {
if (!response.body) {
throw new Error('Response body is null');
}
let lastReportedPercent = -1; // Track last reported percentage to throttle updates
// Read the response stream
const reader = response.body.getReader();
@@ -172,10 +179,15 @@ export class GitHubUpdater {
chunks.push(value);
downloadedSize += value.length;
// Report progress
// Report progress - only when percentage changes by at least 1%
if (totalSize > 0 && onProgress) {
const percent = Math.round((downloadedSize / totalSize) * 100);
onProgress(percent);
// Only report if percent changed (throttles from hundreds/sec to ~100 total)
if (percent !== lastReportedPercent) {
onProgress(percent);
lastReportedPercent = percent;
}
}
}