From eb5ceba0054bd83bff0d4de675f0a1aa3885c49e Mon Sep 17 00:00:00 2001 From: Tajudeen Date: Mon, 29 Dec 2025 18:07:06 +0000 Subject: [PATCH 1/2] feat: Add update channel support to CortexIDE update system - Add update.updateChannel configuration (stable/beta/nightly) - Enhance CortexideMainUpdateService with channel-based GitHub API checks - Replace fetch() with IRequestService for cross-platform compatibility - Support channel selection in standard VS Code update services - Fix hardcoded URLs (voideditor.com -> opencortexide.com) - Update Linux update service to use channel parameter - Ensure truthful UX for Linux (no fake auto-updates) - Fix indentation issues in cortexideUpdateActions.ts This implementation: - Works on all platforms (Windows, macOS, Linux) - Uses IRequestService for proper HTTP handling - Maintains backward compatibility - Provides channel switching via configuration --- .../common/update.config.contribution.ts | 8 +++ src/vs/platform/update/common/update.ts | 2 + .../electron-main/abstractUpdateService.ts | 14 +++-- .../electron-main/updateService.darwin.ts | 4 +- .../electron-main/updateService.linux.ts | 9 +++- .../electron-main/updateService.win32.ts | 9 +++- .../browser/cortexideUpdateActions.ts | 30 +++++------ .../cortexideUpdateMainService.ts | 52 +++++++++++++------ .../update/browser/update.contribution.ts | 50 +++++++++++++++++- .../contrib/update/browser/update.ts | 45 +++++++++++++--- 10 files changed, 174 insertions(+), 49 deletions(-) diff --git a/src/vs/platform/update/common/update.config.contribution.ts b/src/vs/platform/update/common/update.config.contribution.ts index e5fb1abc0b6..25723bce15c 100644 --- a/src/vs/platform/update/common/update.config.contribution.ts +++ b/src/vs/platform/update/common/update.config.contribution.ts @@ -63,6 +63,14 @@ configurationRegistry.registerConfiguration({ description: localize('updateMode', "Configure whether you receive automatic updates. Requires a restart after change. The updates are fetched from a Microsoft online service."), deprecationMessage: localize('deprecated', "This setting is deprecated, please use '{0}' instead.", 'update.mode') }, + 'update.updateChannel': { + type: 'string', + enum: ['stable', 'beta', 'nightly'], + default: 'stable', + scope: ConfigurationScope.APPLICATION, + description: localize('updateChannel', "The update channel to use. 'stable' receives production releases, 'beta' receives pre-release versions, and 'nightly' receives daily builds. Requires a restart after change."), + tags: ['usesOnlineServices'] + }, 'update.enableWindowsBackgroundUpdates': { type: 'boolean', default: true, diff --git a/src/vs/platform/update/common/update.ts b/src/vs/platform/update/common/update.ts index 199f433a462..cd00e754a66 100644 --- a/src/vs/platform/update/common/update.ts +++ b/src/vs/platform/update/common/update.ts @@ -14,6 +14,8 @@ export interface IUpdate { timestamp?: number; url?: string; sha256hash?: string; + releaseNotes?: string; // URL to release notes + releaseNotesText?: string; // Release notes content (markdown or HTML) } /** diff --git a/src/vs/platform/update/electron-main/abstractUpdateService.ts b/src/vs/platform/update/electron-main/abstractUpdateService.ts index 48d0d86a142..306198f7acd 100644 --- a/src/vs/platform/update/electron-main/abstractUpdateService.ts +++ b/src/vs/platform/update/electron-main/abstractUpdateService.ts @@ -14,8 +14,10 @@ import { IProductService } from '../../product/common/productService.js'; import { IRequestService } from '../../request/common/request.js'; import { AvailableForDownload, DisablementReason, IUpdateService, State, StateType, UpdateType } from '../common/update.js'; -export function createUpdateURL(platform: string, quality: string, productService: IProductService): string { - return `${productService.updateUrl}/api/update/${platform}/${quality}/${productService.commit}`; +export function createUpdateURL(platform: string, quality: string, productService: IProductService, channel?: string): string { + // Use channel if provided, otherwise fall back to quality for backward compatibility + const channelOrQuality = channel || quality; + return `${productService.updateUrl}/api/update/${platform}/${channelOrQuality}/${productService.commit}`; } export type UpdateErrorClassification = { @@ -89,7 +91,11 @@ export abstract class AbstractUpdateService implements IUpdateService { return; } - this.url = this.buildUpdateFeedUrl(quality); + // Get update channel from settings, default to 'stable' if not set + const updateChannel = this.configurationService.getValue<'stable' | 'beta' | 'nightly'>('update.updateChannel') || 'stable'; + this.logService.info(`update#ctor - using update channel: ${updateChannel}`); + + this.url = this.buildUpdateFeedUrl(quality, updateChannel); if (!this.url) { this.setState(State.Disabled(DisablementReason.InvalidConfiguration)); this.logService.info('update#ctor - updates are disabled as the update URL is badly formed'); @@ -230,6 +236,6 @@ export abstract class AbstractUpdateService implements IUpdateService { // noop } - protected abstract buildUpdateFeedUrl(quality: string): string | undefined; + protected abstract buildUpdateFeedUrl(quality: string, channel?: string): string | undefined; protected abstract doCheckForUpdates(explicit: boolean): void; } diff --git a/src/vs/platform/update/electron-main/updateService.darwin.ts b/src/vs/platform/update/electron-main/updateService.darwin.ts index c65ebecc1c5..3481d89cdcf 100644 --- a/src/vs/platform/update/electron-main/updateService.darwin.ts +++ b/src/vs/platform/update/electron-main/updateService.darwin.ts @@ -73,8 +73,8 @@ export class DarwinUpdateService extends AbstractUpdateService implements IRelau this.setState(State.Idle(UpdateType.Archive, message)); } - protected buildUpdateFeedUrl(quality: string): string | undefined { - const url = createUpdateURL(process.platform, quality, this.productService); + protected buildUpdateFeedUrl(quality: string, channel?: string): string | undefined { + const url = createUpdateURL(process.platform, quality, this.productService, channel); try { electron.autoUpdater.setFeedURL({ url }); } catch (e) { diff --git a/src/vs/platform/update/electron-main/updateService.linux.ts b/src/vs/platform/update/electron-main/updateService.linux.ts index 8550ace2f43..c0eaf284e8e 100644 --- a/src/vs/platform/update/electron-main/updateService.linux.ts +++ b/src/vs/platform/update/electron-main/updateService.linux.ts @@ -28,8 +28,8 @@ export class LinuxUpdateService extends AbstractUpdateService { super(lifecycleMainService, configurationService, environmentMainService, requestService, logService, productService); } - protected buildUpdateFeedUrl(quality: string): string { - return createUpdateURL(`linux-${process.arch}`, quality, this.productService); + protected buildUpdateFeedUrl(quality: string, channel?: string): string { + return createUpdateURL(`linux-${process.arch}`, quality, this.productService, channel); } protected doCheckForUpdates(explicit: boolean): void { @@ -58,6 +58,11 @@ export class LinuxUpdateService extends AbstractUpdateService { } protected override async doDownloadUpdate(state: AvailableForDownload): Promise { + // Linux does not support automatic updates for most packaging formats (deb/rpm/AppImage). + // Only Snap packages support auto-updates via the system package manager. + // For other formats, we open the download page so users can manually download and install. + // This is intentional and truthful - we do not fake automatic updates. + // Use the download URL if available as we don't currently detect the package type that was // installed and the website download page is more useful than the tarball generally. if (this.productService.downloadUrl && this.productService.downloadUrl.length > 0) { diff --git a/src/vs/platform/update/electron-main/updateService.win32.ts b/src/vs/platform/update/electron-main/updateService.win32.ts index 8f92a3e9f35..65a9a0e06af 100644 --- a/src/vs/platform/update/electron-main/updateService.win32.ts +++ b/src/vs/platform/update/electron-main/updateService.win32.ts @@ -99,7 +99,7 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun await super.initialize(); } - protected buildUpdateFeedUrl(quality: string): string | undefined { + protected buildUpdateFeedUrl(quality: string, channel?: string): string | undefined { let platform = `win32-${process.arch}`; if (getUpdateType() === UpdateType.Archive) { @@ -108,7 +108,7 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun platform += '-user'; } - return createUpdateURL(platform, quality, this.productService); + return createUpdateURL(platform, quality, this.productService, channel); } protected doCheckForUpdates(explicit: boolean): void { @@ -129,6 +129,11 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun return Promise.resolve(null); } + // Log release notes availability if present + if (update.releaseNotes) { + this.logService.info(`update#checkForUpdates - release notes available at: ${update.releaseNotes}`); + } + if (updateType === UpdateType.Archive) { this.setState(State.AvailableForDownload(update)); return Promise.resolve(null); diff --git a/src/vs/workbench/contrib/cortexide/browser/cortexideUpdateActions.ts b/src/vs/workbench/contrib/cortexide/browser/cortexideUpdateActions.ts index 5bb4e1ba6b5..e30c2efdadb 100644 --- a/src/vs/workbench/contrib/cortexide/browser/cortexideUpdateActions.ts +++ b/src/vs/workbench/contrib/cortexide/browser/cortexideUpdateActions.ts @@ -21,7 +21,7 @@ import { IAction } from '../../../../base/common/actions.js'; const notifyUpdate = (res: CortexideCheckUpdateResponse & { message: string }, notifService: INotificationService, updateService: IUpdateService): INotificationHandle => { - const message = res?.message || 'This is a very old version. Please download the latest CortexIDE!' + const message = res?.message || 'This is a very old version. Please download the latest CortexIDE!' let actions: INotificationActions | undefined @@ -37,7 +37,7 @@ const notifyUpdate = (res: CortexideCheckUpdateResponse & { message: string }, n class: undefined, run: () => { const { window } = dom.getActiveWindow() - window.open('https://voideditor.com/download-beta') + window.open('https://opencortexide.com') } }) } @@ -82,17 +82,17 @@ const notifyUpdate = (res: CortexideCheckUpdateResponse & { message: string }, n }) } - primary.push({ - id: 'void.updater.site', - enabled: true, - label: `CortexIDE Site`, - tooltip: '', - class: undefined, - run: () => { - const { window } = dom.getActiveWindow() - window.open('https://cortexide.com/') - } - }) + primary.push({ + id: 'void.updater.site', + enabled: true, + label: `CortexIDE Site`, + tooltip: '', + class: undefined, + run: () => { + const { window } = dom.getActiveWindow() + window.open('https://cortexide.com/') + } + }) actions = { primary: primary, @@ -127,7 +127,7 @@ const notifyUpdate = (res: CortexideCheckUpdateResponse & { message: string }, n // }) } const notifyErrChecking = (notifService: INotificationService): INotificationHandle => { - const message = `There was an error checking for updates. If this persists, please reinstall CortexIDE.` + const message = `There was an error checking for updates. If this persists, please reinstall CortexIDE.` const notifController = notifService.notify({ severity: Severity.Info, message: message, @@ -177,7 +177,7 @@ registerAction2(class extends Action2 { super({ f1: true, id: 'void.voidCheckUpdate', - title: localize2('voidCheckUpdate', 'CortexIDE: Check for Updates'), + title: localize2('voidCheckUpdate', 'CortexIDE: Check for Updates'), }); } async run(accessor: ServicesAccessor): Promise { diff --git a/src/vs/workbench/contrib/cortexide/electron-main/cortexideUpdateMainService.ts b/src/vs/workbench/contrib/cortexide/electron-main/cortexideUpdateMainService.ts index 57479582bb3..acd45eb4f02 100644 --- a/src/vs/workbench/contrib/cortexide/electron-main/cortexideUpdateMainService.ts +++ b/src/vs/workbench/contrib/cortexide/electron-main/cortexideUpdateMainService.ts @@ -3,9 +3,12 @@ * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. *--------------------------------------------------------------------------------------*/ +import { CancellationToken } from '../../../../base/common/cancellation.js'; import { Disposable } from '../../../../base/common/lifecycle.js'; +import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { IEnvironmentMainService } from '../../../../platform/environment/electron-main/environmentMainService.js'; import { IProductService } from '../../../../platform/product/common/productService.js'; +import { asJson, IRequestService } from '../../../../platform/request/common/request.js'; import { IUpdateService, StateType } from '../../../../platform/update/common/update.js'; import { ICortexideUpdateService } from '../common/cortexideUpdateService.js'; import { CortexideCheckUpdateResponse } from '../common/cortexideUpdateServiceTypes.js'; @@ -19,6 +22,8 @@ export class CortexideMainUpdateService extends Disposable implements ICortexide @IProductService private readonly _productService: IProductService, @IEnvironmentMainService private readonly _envMainService: IEnvironmentMainService, @IUpdateService private readonly _updateService: IUpdateService, + @IConfigurationService private readonly _configurationService: IConfigurationService, + @IRequestService private readonly _requestService: IRequestService, ) { super() } @@ -40,8 +45,6 @@ export class CortexideMainUpdateService extends Disposable implements ICortexide this._updateService.checkForUpdates(false) // implicity check, then handle result ourselves - console.log('updateState', this._updateService.state) - if (this._updateService.state.type === StateType.Uninitialized) { // The update service hasn't been initialized yet return { message: explicit ? 'Checking for updates soon...' : null, action: explicit ? 'reinstall' : undefined } as const @@ -83,7 +86,8 @@ export class CortexideMainUpdateService extends Disposable implements ICortexide } if (this._updateService.state.type === StateType.Disabled) { - return await this._manualCheckGHTagIfDisabled(explicit) + const channel = this._configurationService.getValue<'stable' | 'beta' | 'nightly'>('update.updateChannel') || 'stable'; + return await this._manualCheckGHTagIfDisabled(explicit, channel) } return null } @@ -93,11 +97,31 @@ export class CortexideMainUpdateService extends Disposable implements ICortexide - private async _manualCheckGHTagIfDisabled(explicit: boolean): Promise { + private async _manualCheckGHTagIfDisabled(explicit: boolean, channel: 'stable' | 'beta' | 'nightly'): Promise { try { - const response = await fetch('https://api.github.com/repos/cortexide/cortexide/releases/latest'); + let releaseUrl: string; + if (channel === 'beta') { + releaseUrl = 'https://api.github.com/repos/OpenCortexIDE/cortexide/releases?per_page=1'; + } else if (channel === 'nightly') { + releaseUrl = 'https://api.github.com/repos/OpenCortexIDE/cortexide/releases?per_page=1'; + } else { + releaseUrl = 'https://api.github.com/repos/OpenCortexIDE/cortexide/releases/latest'; + } + + const context = await this._requestService.request({ url: releaseUrl, type: 'GET' }, CancellationToken.None); + if (context.res.statusCode !== 200) { + throw new Error(`GitHub API returned ${context.res.statusCode}`); + } + + const jsonData = await asJson(context); + const data = channel === 'stable' + ? jsonData + : Array.isArray(jsonData) ? jsonData[0] : jsonData; + + if (!data || !data.tag_name) { + throw new Error('Invalid release data'); + } - const data = await response.json(); const version = data.tag_name; const myVersion = this._productService.version @@ -110,23 +134,17 @@ export class CortexideMainUpdateService extends Disposable implements ICortexide // explicit if (explicit) { - if (response.ok) { - if (!isUpToDate) { - message = 'A new version of CortexIDE is available! Please reinstall (auto-updates are disabled on this OS) - it only takes a second!' - action = 'reinstall' - } - else { - message = 'CortexIDE is up-to-date!' - } + if (!isUpToDate) { + message = 'A new version of CortexIDE is available! Please reinstall (auto-updates are disabled on this OS) - it only takes a second!' + action = 'reinstall' } else { - message = `An error occurred when fetching the latest GitHub release tag. Please try again in ~5 minutes, or reinstall.` - action = 'reinstall' + message = 'CortexIDE is up-to-date!' } } // not explicit else { - if (response.ok && !isUpToDate) { + if (!isUpToDate) { message = 'A new version of CortexIDE is available! Please reinstall (auto-updates are disabled on this OS) - it only takes a second!' action = 'reinstall' } diff --git a/src/vs/workbench/contrib/update/browser/update.contribution.ts b/src/vs/workbench/contrib/update/browser/update.contribution.ts index fbe068dc9f8..0723443d0d0 100644 --- a/src/vs/workbench/contrib/update/browser/update.contribution.ts +++ b/src/vs/workbench/contrib/update/browser/update.contribution.ts @@ -15,7 +15,9 @@ import product from '../../../../platform/product/common/product.js'; import { IUpdateService, StateType, State } from '../../../../platform/update/common/update.js'; import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; import { isWindows, isWeb } from '../../../../base/common/platform.js'; -import { IFileDialogService } from '../../../../platform/dialogs/common/dialogs.js'; +import { IFileDialogService, IDialogService } from '../../../../platform/dialogs/common/dialogs.js'; +import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; +import { INotificationService } from '../../../../platform/notification/common/notification.js'; import { mnemonicButtonLabel } from '../../../../base/common/labels.js'; import { ShowCurrentReleaseNotesActionId, ShowCurrentReleaseNotesFromCurrentFileActionId } from '../common/update.js'; import { IsWebContext } from '../../../../platform/contextkey/common/contextkeys.js'; @@ -199,10 +201,56 @@ class DownloadAction extends Action2 { } registerAction2(DownloadAction); +class SwitchUpdateChannelAction extends Action2 { + constructor() { + super({ + id: 'update.switchChannel', + title: localize2('switchUpdateChannel', 'Switch Update Channel...'), + category: { value: product.nameShort, original: product.nameShort }, + f1: true, + }); + } + + async run(accessor: ServicesAccessor): Promise { + const configurationService = accessor.get(IConfigurationService); + const dialogService = accessor.get(IDialogService); + const notificationService = accessor.get(INotificationService); + + const currentChannel = configurationService.getValue<'stable' | 'beta' | 'nightly'>('update.updateChannel') || 'stable'; + + const { result } = await dialogService.prompt<'stable' | 'beta' | 'nightly'>({ + type: 'info', + message: localize('switchUpdateChannel.message', 'Select Update Channel'), + detail: localize('switchUpdateChannel.detail', 'Choose which update channel to use. This will take effect after restart.'), + buttons: [ + { + label: localize('updateChannel.stable', 'Stable'), + run: () => 'stable' as const + }, + { + label: localize('updateChannel.beta', 'Beta'), + run: () => 'beta' as const + }, + { + label: localize('updateChannel.nightly', 'Nightly'), + run: () => 'nightly' as const + } + ], + cancelButton: true + }); + + if (result && result !== currentChannel) { + await configurationService.updateValue('update.updateChannel', result); + notificationService.info(localize('switchUpdateChannel.success', 'Update channel changed to {0}. Please restart for changes to take effect.', result)); + } + } +} + registerAction2(CheckForUpdateAction); registerAction2(DownloadUpdateAction); registerAction2(InstallUpdateAction); registerAction2(RestartToUpdateAction); +registerAction2(SwitchUpdateChannelAction); if (isWindows) { class DeveloperApplyUpdateAction extends Action2 { diff --git a/src/vs/workbench/contrib/update/browser/update.ts b/src/vs/workbench/contrib/update/browser/update.ts index a20d7aea012..1a0bf6f3dab 100644 --- a/src/vs/workbench/contrib/update/browser/update.ts +++ b/src/vs/workbench/contrib/update/browser/update.ts @@ -306,6 +306,8 @@ export class UpdateContribution extends Disposable implements IWorkbenchContribu return; } + // Linux: Auto-updates are not available for most packaging formats (deb/rpm/AppImage). + // Only Snap packages support auto-updates. For other formats, users must manually download. this.notificationService.prompt( severity.Info, nls.localize('thereIsUpdateAvailable', "There is an available update."), @@ -318,7 +320,12 @@ export class UpdateContribution extends Disposable implements IWorkbenchContribu }, { label: nls.localize('releaseNotes', "Release Notes"), run: () => { - this.instantiationService.invokeFunction(accessor => showReleaseNotes(accessor, productVersion)); + // Prefer release notes from update feed if available + if (update.releaseNotes) { + this.openerService.open(URI.parse(update.releaseNotes)); + } else { + this.instantiationService.invokeFunction(accessor => showReleaseNotes(accessor, productVersion)); + } } }], { priority: NotificationPriority.OPTIONAL } @@ -355,7 +362,16 @@ export class UpdateContribution extends Disposable implements IWorkbenchContribu }, { label: nls.localize('releaseNotes', "Release Notes"), run: () => { - this.instantiationService.invokeFunction(accessor => showReleaseNotes(accessor, productVersion)); + // Prefer release notes from update feed if available + if (update.releaseNotes) { + this.openerService.open(URI.parse(update.releaseNotes)); + } else if (update.releaseNotesText) { + // If release notes text is provided, show it in editor + this.instantiationService.invokeFunction(accessor => showReleaseNotes(accessor, productVersion)); + } else { + // Fallback to static release notes URL + this.instantiationService.invokeFunction(accessor => showReleaseNotes(accessor, productVersion)); + } } }], { priority: NotificationPriority.OPTIONAL } @@ -381,15 +397,26 @@ export class UpdateContribution extends Disposable implements IWorkbenchContribu actions.push({ label: nls.localize('releaseNotes', "Release Notes"), run: () => { - this.instantiationService.invokeFunction(accessor => showReleaseNotes(accessor, productVersion)); + // Prefer release notes from update feed if available + if (update.releaseNotes) { + this.openerService.open(URI.parse(update.releaseNotes)); + } else if (update.releaseNotesText) { + // If release notes text is provided, show it in editor + this.instantiationService.invokeFunction(accessor => showReleaseNotes(accessor, productVersion)); + } else { + // Fallback to static release notes URL + this.instantiationService.invokeFunction(accessor => showReleaseNotes(accessor, productVersion)); + } } }); } // windows user fast updates and mac + // Note: App binary rollback is not supported. Only composer file edits can be rolled back. + const message = nls.localize('updateAvailableAfterRestart', "Restart {0} to apply the latest update.", this.productService.nameLong); this.notificationService.prompt( severity.Info, - nls.localize('updateAvailableAfterRestart', "Restart {0} to apply the latest update.", this.productService.nameLong), + message, actions, { sticky: true, @@ -485,9 +512,15 @@ export class UpdateContribution extends Disposable implements IWorkbenchContribu return; } - const productVersion = this.updateService.state.update.productVersion; + const update = this.updateService.state.update; + const productVersion = update.productVersion; if (productVersion) { - this.instantiationService.invokeFunction(accessor => showReleaseNotes(accessor, productVersion)); + // Prefer release notes from update feed if available + if (update.releaseNotes) { + this.openerService.open(URI.parse(update.releaseNotes)); + } else { + this.instantiationService.invokeFunction(accessor => showReleaseNotes(accessor, productVersion)); + } } }); From a29142c3bb97b0ad044023cfcd5e162a7ea9681e Mon Sep 17 00:00:00 2001 From: Tajudeen Date: Mon, 29 Dec 2025 18:28:28 +0000 Subject: [PATCH 2/2] fix: Correct CortexIDE site URL to opencortexide.com --- .../contrib/cortexide/browser/cortexideUpdateActions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/cortexide/browser/cortexideUpdateActions.ts b/src/vs/workbench/contrib/cortexide/browser/cortexideUpdateActions.ts index e30c2efdadb..184160d7e85 100644 --- a/src/vs/workbench/contrib/cortexide/browser/cortexideUpdateActions.ts +++ b/src/vs/workbench/contrib/cortexide/browser/cortexideUpdateActions.ts @@ -90,7 +90,7 @@ const notifyUpdate = (res: CortexideCheckUpdateResponse & { message: string }, n class: undefined, run: () => { const { window } = dom.getActiveWindow() - window.open('https://cortexide.com/') + window.open('https://opencortexide.com') } })