Skip to content

Commit 67fe028

Browse files
authored
Merge pull request #16 from OpenCortexIDE/feature/auto-update-system
feat: Add update channel support to CortexIDE update system
2 parents f4522a6 + a29142c commit 67fe028

10 files changed

Lines changed: 174 additions & 49 deletions

File tree

src/vs/platform/update/common/update.config.contribution.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,14 @@ configurationRegistry.registerConfiguration({
6363
description: localize('updateMode', "Configure whether you receive automatic updates. Requires a restart after change. The updates are fetched from a Microsoft online service."),
6464
deprecationMessage: localize('deprecated', "This setting is deprecated, please use '{0}' instead.", 'update.mode')
6565
},
66+
'update.updateChannel': {
67+
type: 'string',
68+
enum: ['stable', 'beta', 'nightly'],
69+
default: 'stable',
70+
scope: ConfigurationScope.APPLICATION,
71+
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."),
72+
tags: ['usesOnlineServices']
73+
},
6674
'update.enableWindowsBackgroundUpdates': {
6775
type: 'boolean',
6876
default: true,

src/vs/platform/update/common/update.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ export interface IUpdate {
1414
timestamp?: number;
1515
url?: string;
1616
sha256hash?: string;
17+
releaseNotes?: string; // URL to release notes
18+
releaseNotesText?: string; // Release notes content (markdown or HTML)
1719
}
1820

1921
/**

src/vs/platform/update/electron-main/abstractUpdateService.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,10 @@ import { IProductService } from '../../product/common/productService.js';
1414
import { IRequestService } from '../../request/common/request.js';
1515
import { AvailableForDownload, DisablementReason, IUpdateService, State, StateType, UpdateType } from '../common/update.js';
1616

17-
export function createUpdateURL(platform: string, quality: string, productService: IProductService): string {
18-
return `${productService.updateUrl}/api/update/${platform}/${quality}/${productService.commit}`;
17+
export function createUpdateURL(platform: string, quality: string, productService: IProductService, channel?: string): string {
18+
// Use channel if provided, otherwise fall back to quality for backward compatibility
19+
const channelOrQuality = channel || quality;
20+
return `${productService.updateUrl}/api/update/${platform}/${channelOrQuality}/${productService.commit}`;
1921
}
2022

2123
export type UpdateErrorClassification = {
@@ -89,7 +91,11 @@ export abstract class AbstractUpdateService implements IUpdateService {
8991
return;
9092
}
9193

92-
this.url = this.buildUpdateFeedUrl(quality);
94+
// Get update channel from settings, default to 'stable' if not set
95+
const updateChannel = this.configurationService.getValue<'stable' | 'beta' | 'nightly'>('update.updateChannel') || 'stable';
96+
this.logService.info(`update#ctor - using update channel: ${updateChannel}`);
97+
98+
this.url = this.buildUpdateFeedUrl(quality, updateChannel);
9399
if (!this.url) {
94100
this.setState(State.Disabled(DisablementReason.InvalidConfiguration));
95101
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 {
230236
// noop
231237
}
232238

233-
protected abstract buildUpdateFeedUrl(quality: string): string | undefined;
239+
protected abstract buildUpdateFeedUrl(quality: string, channel?: string): string | undefined;
234240
protected abstract doCheckForUpdates(explicit: boolean): void;
235241
}

src/vs/platform/update/electron-main/updateService.darwin.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,8 @@ export class DarwinUpdateService extends AbstractUpdateService implements IRelau
7373
this.setState(State.Idle(UpdateType.Archive, message));
7474
}
7575

76-
protected buildUpdateFeedUrl(quality: string): string | undefined {
77-
const url = createUpdateURL(process.platform, quality, this.productService);
76+
protected buildUpdateFeedUrl(quality: string, channel?: string): string | undefined {
77+
const url = createUpdateURL(process.platform, quality, this.productService, channel);
7878
try {
7979
electron.autoUpdater.setFeedURL({ url });
8080
} catch (e) {

src/vs/platform/update/electron-main/updateService.linux.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ export class LinuxUpdateService extends AbstractUpdateService {
2828
super(lifecycleMainService, configurationService, environmentMainService, requestService, logService, productService);
2929
}
3030

31-
protected buildUpdateFeedUrl(quality: string): string {
32-
return createUpdateURL(`linux-${process.arch}`, quality, this.productService);
31+
protected buildUpdateFeedUrl(quality: string, channel?: string): string {
32+
return createUpdateURL(`linux-${process.arch}`, quality, this.productService, channel);
3333
}
3434

3535
protected doCheckForUpdates(explicit: boolean): void {
@@ -58,6 +58,11 @@ export class LinuxUpdateService extends AbstractUpdateService {
5858
}
5959

6060
protected override async doDownloadUpdate(state: AvailableForDownload): Promise<void> {
61+
// Linux does not support automatic updates for most packaging formats (deb/rpm/AppImage).
62+
// Only Snap packages support auto-updates via the system package manager.
63+
// For other formats, we open the download page so users can manually download and install.
64+
// This is intentional and truthful - we do not fake automatic updates.
65+
6166
// Use the download URL if available as we don't currently detect the package type that was
6267
// installed and the website download page is more useful than the tarball generally.
6368
if (this.productService.downloadUrl && this.productService.downloadUrl.length > 0) {

src/vs/platform/update/electron-main/updateService.win32.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun
9999
await super.initialize();
100100
}
101101

102-
protected buildUpdateFeedUrl(quality: string): string | undefined {
102+
protected buildUpdateFeedUrl(quality: string, channel?: string): string | undefined {
103103
let platform = `win32-${process.arch}`;
104104

105105
if (getUpdateType() === UpdateType.Archive) {
@@ -108,7 +108,7 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun
108108
platform += '-user';
109109
}
110110

111-
return createUpdateURL(platform, quality, this.productService);
111+
return createUpdateURL(platform, quality, this.productService, channel);
112112
}
113113

114114
protected doCheckForUpdates(explicit: boolean): void {
@@ -129,6 +129,11 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun
129129
return Promise.resolve(null);
130130
}
131131

132+
// Log release notes availability if present
133+
if (update.releaseNotes) {
134+
this.logService.info(`update#checkForUpdates - release notes available at: ${update.releaseNotes}`);
135+
}
136+
132137
if (updateType === UpdateType.Archive) {
133138
this.setState(State.AvailableForDownload(update));
134139
return Promise.resolve(null);

src/vs/workbench/contrib/cortexide/browser/cortexideUpdateActions.ts

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import { IAction } from '../../../../base/common/actions.js';
2121

2222

2323
const notifyUpdate = (res: CortexideCheckUpdateResponse & { message: string }, notifService: INotificationService, updateService: IUpdateService): INotificationHandle => {
24-
const message = res?.message || 'This is a very old version. Please download the latest CortexIDE!'
24+
const message = res?.message || 'This is a very old version. Please download the latest CortexIDE!'
2525

2626
let actions: INotificationActions | undefined
2727

@@ -37,7 +37,7 @@ const notifyUpdate = (res: CortexideCheckUpdateResponse & { message: string }, n
3737
class: undefined,
3838
run: () => {
3939
const { window } = dom.getActiveWindow()
40-
window.open('https://voideditor.com/download-beta')
40+
window.open('https://opencortexide.com')
4141
}
4242
})
4343
}
@@ -82,17 +82,17 @@ const notifyUpdate = (res: CortexideCheckUpdateResponse & { message: string }, n
8282
})
8383
}
8484

85-
primary.push({
86-
id: 'void.updater.site',
87-
enabled: true,
88-
label: `CortexIDE Site`,
89-
tooltip: '',
90-
class: undefined,
91-
run: () => {
92-
const { window } = dom.getActiveWindow()
93-
window.open('https://cortexide.com/')
94-
}
95-
})
85+
primary.push({
86+
id: 'void.updater.site',
87+
enabled: true,
88+
label: `CortexIDE Site`,
89+
tooltip: '',
90+
class: undefined,
91+
run: () => {
92+
const { window } = dom.getActiveWindow()
93+
window.open('https://opencortexide.com')
94+
}
95+
})
9696

9797
actions = {
9898
primary: primary,
@@ -127,7 +127,7 @@ const notifyUpdate = (res: CortexideCheckUpdateResponse & { message: string }, n
127127
// })
128128
}
129129
const notifyErrChecking = (notifService: INotificationService): INotificationHandle => {
130-
const message = `There was an error checking for updates. If this persists, please reinstall CortexIDE.`
130+
const message = `There was an error checking for updates. If this persists, please reinstall CortexIDE.`
131131
const notifController = notifService.notify({
132132
severity: Severity.Info,
133133
message: message,
@@ -177,7 +177,7 @@ registerAction2(class extends Action2 {
177177
super({
178178
f1: true,
179179
id: 'void.voidCheckUpdate',
180-
title: localize2('voidCheckUpdate', 'CortexIDE: Check for Updates'),
180+
title: localize2('voidCheckUpdate', 'CortexIDE: Check for Updates'),
181181
});
182182
}
183183
async run(accessor: ServicesAccessor): Promise<void> {

src/vs/workbench/contrib/cortexide/electron-main/cortexideUpdateMainService.ts

Lines changed: 35 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,12 @@
33
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
44
*--------------------------------------------------------------------------------------*/
55

6+
import { CancellationToken } from '../../../../base/common/cancellation.js';
67
import { Disposable } from '../../../../base/common/lifecycle.js';
8+
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
79
import { IEnvironmentMainService } from '../../../../platform/environment/electron-main/environmentMainService.js';
810
import { IProductService } from '../../../../platform/product/common/productService.js';
11+
import { asJson, IRequestService } from '../../../../platform/request/common/request.js';
912
import { IUpdateService, StateType } from '../../../../platform/update/common/update.js';
1013
import { ICortexideUpdateService } from '../common/cortexideUpdateService.js';
1114
import { CortexideCheckUpdateResponse } from '../common/cortexideUpdateServiceTypes.js';
@@ -19,6 +22,8 @@ export class CortexideMainUpdateService extends Disposable implements ICortexide
1922
@IProductService private readonly _productService: IProductService,
2023
@IEnvironmentMainService private readonly _envMainService: IEnvironmentMainService,
2124
@IUpdateService private readonly _updateService: IUpdateService,
25+
@IConfigurationService private readonly _configurationService: IConfigurationService,
26+
@IRequestService private readonly _requestService: IRequestService,
2227
) {
2328
super()
2429
}
@@ -40,8 +45,6 @@ export class CortexideMainUpdateService extends Disposable implements ICortexide
4045

4146
this._updateService.checkForUpdates(false) // implicity check, then handle result ourselves
4247

43-
console.log('updateState', this._updateService.state)
44-
4548
if (this._updateService.state.type === StateType.Uninitialized) {
4649
// The update service hasn't been initialized yet
4750
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
8386
}
8487

8588
if (this._updateService.state.type === StateType.Disabled) {
86-
return await this._manualCheckGHTagIfDisabled(explicit)
89+
const channel = this._configurationService.getValue<'stable' | 'beta' | 'nightly'>('update.updateChannel') || 'stable';
90+
return await this._manualCheckGHTagIfDisabled(explicit, channel)
8791
}
8892
return null
8993
}
@@ -93,11 +97,31 @@ export class CortexideMainUpdateService extends Disposable implements ICortexide
9397

9498

9599

96-
private async _manualCheckGHTagIfDisabled(explicit: boolean): Promise<CortexideCheckUpdateResponse> {
100+
private async _manualCheckGHTagIfDisabled(explicit: boolean, channel: 'stable' | 'beta' | 'nightly'): Promise<CortexideCheckUpdateResponse> {
97101
try {
98-
const response = await fetch('https://api.github.com/repos/cortexide/cortexide/releases/latest');
102+
let releaseUrl: string;
103+
if (channel === 'beta') {
104+
releaseUrl = 'https://api.github.com/repos/OpenCortexIDE/cortexide/releases?per_page=1';
105+
} else if (channel === 'nightly') {
106+
releaseUrl = 'https://api.github.com/repos/OpenCortexIDE/cortexide/releases?per_page=1';
107+
} else {
108+
releaseUrl = 'https://api.github.com/repos/OpenCortexIDE/cortexide/releases/latest';
109+
}
110+
111+
const context = await this._requestService.request({ url: releaseUrl, type: 'GET' }, CancellationToken.None);
112+
if (context.res.statusCode !== 200) {
113+
throw new Error(`GitHub API returned ${context.res.statusCode}`);
114+
}
115+
116+
const jsonData = await asJson(context);
117+
const data = channel === 'stable'
118+
? jsonData
119+
: Array.isArray(jsonData) ? jsonData[0] : jsonData;
120+
121+
if (!data || !data.tag_name) {
122+
throw new Error('Invalid release data');
123+
}
99124

100-
const data = await response.json();
101125
const version = data.tag_name;
102126

103127
const myVersion = this._productService.version
@@ -110,23 +134,17 @@ export class CortexideMainUpdateService extends Disposable implements ICortexide
110134

111135
// explicit
112136
if (explicit) {
113-
if (response.ok) {
114-
if (!isUpToDate) {
115-
message = 'A new version of CortexIDE is available! Please reinstall (auto-updates are disabled on this OS) - it only takes a second!'
116-
action = 'reinstall'
117-
}
118-
else {
119-
message = 'CortexIDE is up-to-date!'
120-
}
137+
if (!isUpToDate) {
138+
message = 'A new version of CortexIDE is available! Please reinstall (auto-updates are disabled on this OS) - it only takes a second!'
139+
action = 'reinstall'
121140
}
122141
else {
123-
message = `An error occurred when fetching the latest GitHub release tag. Please try again in ~5 minutes, or reinstall.`
124-
action = 'reinstall'
142+
message = 'CortexIDE is up-to-date!'
125143
}
126144
}
127145
// not explicit
128146
else {
129-
if (response.ok && !isUpToDate) {
147+
if (!isUpToDate) {
130148
message = 'A new version of CortexIDE is available! Please reinstall (auto-updates are disabled on this OS) - it only takes a second!'
131149
action = 'reinstall'
132150
}

src/vs/workbench/contrib/update/browser/update.contribution.ts

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@ import product from '../../../../platform/product/common/product.js';
1515
import { IUpdateService, StateType, State } from '../../../../platform/update/common/update.js';
1616
import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js';
1717
import { isWindows, isWeb } from '../../../../base/common/platform.js';
18-
import { IFileDialogService } from '../../../../platform/dialogs/common/dialogs.js';
18+
import { IFileDialogService, IDialogService } from '../../../../platform/dialogs/common/dialogs.js';
19+
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
20+
import { INotificationService } from '../../../../platform/notification/common/notification.js';
1921
import { mnemonicButtonLabel } from '../../../../base/common/labels.js';
2022
import { ShowCurrentReleaseNotesActionId, ShowCurrentReleaseNotesFromCurrentFileActionId } from '../common/update.js';
2123
import { IsWebContext } from '../../../../platform/contextkey/common/contextkeys.js';
@@ -199,10 +201,56 @@ class DownloadAction extends Action2 {
199201
}
200202

201203
registerAction2(DownloadAction);
204+
class SwitchUpdateChannelAction extends Action2 {
205+
constructor() {
206+
super({
207+
id: 'update.switchChannel',
208+
title: localize2('switchUpdateChannel', 'Switch Update Channel...'),
209+
category: { value: product.nameShort, original: product.nameShort },
210+
f1: true,
211+
});
212+
}
213+
214+
async run(accessor: ServicesAccessor): Promise<void> {
215+
const configurationService = accessor.get(IConfigurationService);
216+
const dialogService = accessor.get(IDialogService);
217+
const notificationService = accessor.get(INotificationService);
218+
219+
const currentChannel = configurationService.getValue<'stable' | 'beta' | 'nightly'>('update.updateChannel') || 'stable';
220+
221+
const { result } = await dialogService.prompt<'stable' | 'beta' | 'nightly'>({
222+
type: 'info',
223+
message: localize('switchUpdateChannel.message', 'Select Update Channel'),
224+
detail: localize('switchUpdateChannel.detail', 'Choose which update channel to use. This will take effect after restart.'),
225+
buttons: [
226+
{
227+
label: localize('updateChannel.stable', 'Stable'),
228+
run: () => 'stable' as const
229+
},
230+
{
231+
label: localize('updateChannel.beta', 'Beta'),
232+
run: () => 'beta' as const
233+
},
234+
{
235+
label: localize('updateChannel.nightly', 'Nightly'),
236+
run: () => 'nightly' as const
237+
}
238+
],
239+
cancelButton: true
240+
});
241+
242+
if (result && result !== currentChannel) {
243+
await configurationService.updateValue('update.updateChannel', result);
244+
notificationService.info(localize('switchUpdateChannel.success', 'Update channel changed to {0}. Please restart for changes to take effect.', result));
245+
}
246+
}
247+
}
248+
202249
registerAction2(CheckForUpdateAction);
203250
registerAction2(DownloadUpdateAction);
204251
registerAction2(InstallUpdateAction);
205252
registerAction2(RestartToUpdateAction);
253+
registerAction2(SwitchUpdateChannelAction);
206254

207255
if (isWindows) {
208256
class DeveloperApplyUpdateAction extends Action2 {

0 commit comments

Comments
 (0)