Skip to content

Commit c6ba7e0

Browse files
authored
feat(updater): switch app auto-update feed from GitHub Releases to CDN (#1543)
1 parent 7ced44d commit c6ba7e0

3 files changed

Lines changed: 282 additions & 12 deletions

File tree

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
2+
// Licensed under the Apache License, Version 2.0 (the "License");
3+
// you may not use this file except in compliance with the License.
4+
// You may obtain a copy of the License at
5+
//
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
14+
15+
import type {
16+
AppUpdater,
17+
ResolvedUpdateFileInfo,
18+
UpdateInfo,
19+
} from 'electron-updater';
20+
import type { ProviderRuntimeOptions } from 'electron-updater/out/providers/Provider';
21+
import { createRequire } from 'node:module';
22+
23+
const require = createRequire(import.meta.url);
24+
25+
const { GitHubProvider } =
26+
require('electron-updater/out/providers/GitHubProvider') as {
27+
GitHubProvider: new (
28+
options: GitHubProviderOptions,
29+
updater: AppUpdater,
30+
runtimeOptions: ProviderRuntimeOptions
31+
) => object;
32+
};
33+
34+
const { resolveFiles } = require('electron-updater/out/providers/Provider') as {
35+
resolveFiles: (
36+
updateInfo: UpdateInfo,
37+
baseUrl: URL,
38+
pathTransformer?: (path: string) => string
39+
) => Array<ResolvedUpdateFileInfo>;
40+
};
41+
42+
export const DEFAULT_CDN_RELEASE_BASE_URL = 'https://cdn.eigent.ai/releases';
43+
44+
export type UpdatePlatformDirectory =
45+
| 'mac-arm64'
46+
| 'mac-intel'
47+
| 'win-x64'
48+
| 'linux-x64';
49+
50+
type GitHubProviderOptions = {
51+
provider: 'github';
52+
owner: string;
53+
repo: string;
54+
releaseType?: 'draft' | 'prerelease' | 'release' | null;
55+
channel?: string | null;
56+
host?: string | null;
57+
protocol?: 'https' | 'http' | null;
58+
token?: string | null;
59+
private?: boolean | null;
60+
tagNamePrefix?: string;
61+
vPrefixedTagName?: boolean;
62+
};
63+
64+
export type GitHubReleaseCdnOptions = {
65+
owner: string;
66+
repo: string;
67+
releaseType?: 'draft' | 'prerelease' | 'release' | null;
68+
channel?: string | null;
69+
host?: string | null;
70+
protocol?: 'https' | 'http' | null;
71+
token?: string | null;
72+
private?: boolean | null;
73+
tagNamePrefix?: string;
74+
vPrefixedTagName?: boolean;
75+
cdnBaseUrl: string;
76+
platformDir: UpdatePlatformDirectory;
77+
};
78+
79+
export function normalizeCdnReleaseBaseUrl(url: string): string {
80+
return url.replace(/\/+$/, '');
81+
}
82+
83+
export function getUpdatePlatformDirectory(
84+
platform: NodeJS.Platform,
85+
arch: NodeJS.Architecture
86+
): UpdatePlatformDirectory | null {
87+
switch (platform) {
88+
case 'darwin':
89+
if (arch === 'arm64') {
90+
return 'mac-arm64';
91+
}
92+
if (arch === 'x64') {
93+
return 'mac-intel';
94+
}
95+
return null;
96+
case 'win32':
97+
return arch === 'x64' ? 'win-x64' : null;
98+
case 'linux':
99+
return arch === 'x64' ? 'linux-x64' : null;
100+
default:
101+
return null;
102+
}
103+
}
104+
105+
export function getGitHubReleaseChannel(
106+
platform: NodeJS.Platform,
107+
arch: NodeJS.Architecture
108+
): string {
109+
if (platform === 'darwin') {
110+
return arch === 'arm64' ? 'latest-arm64' : 'latest-x64';
111+
}
112+
113+
return 'latest';
114+
}
115+
116+
export function buildVersionedReleaseBaseUrl(
117+
cdnBaseUrl: string,
118+
version: string,
119+
platformDir: UpdatePlatformDirectory
120+
): string {
121+
return `${normalizeCdnReleaseBaseUrl(cdnBaseUrl)}/v${version}/${platformDir}/`;
122+
}
123+
124+
export class GitHubReleaseCdnProvider extends GitHubProvider {
125+
private readonly cdnBaseUrl: string;
126+
private readonly platformDir: UpdatePlatformDirectory;
127+
128+
constructor(
129+
options: GitHubReleaseCdnOptions,
130+
updater: AppUpdater,
131+
runtimeOptions: ProviderRuntimeOptions
132+
) {
133+
const githubOptions: GitHubProviderOptions = {
134+
provider: 'github',
135+
owner: options.owner,
136+
repo: options.repo,
137+
releaseType: options.releaseType,
138+
channel: options.channel,
139+
host: options.host,
140+
protocol: options.protocol,
141+
token: options.token,
142+
private: options.private,
143+
tagNamePrefix: options.tagNamePrefix,
144+
vPrefixedTagName: options.vPrefixedTagName,
145+
};
146+
147+
super(githubOptions, updater, runtimeOptions);
148+
149+
this.cdnBaseUrl = normalizeCdnReleaseBaseUrl(options.cdnBaseUrl);
150+
this.platformDir = options.platformDir;
151+
}
152+
153+
resolveFiles(updateInfo: UpdateInfo): Array<ResolvedUpdateFileInfo> {
154+
const versionedBaseUrl = new URL(
155+
buildVersionedReleaseBaseUrl(
156+
this.cdnBaseUrl,
157+
updateInfo.version,
158+
this.platformDir
159+
)
160+
);
161+
162+
return resolveFiles(updateInfo, versionedBaseUrl, (filePath: string) =>
163+
filePath.replace(/ /g, '-')
164+
);
165+
}
166+
}

electron/main/update.ts

Lines changed: 49 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,12 @@ import type {
1919
UpdateInfo,
2020
} from 'electron-updater';
2121
import { createRequire } from 'node:module';
22+
import {
23+
DEFAULT_CDN_RELEASE_BASE_URL,
24+
getGitHubReleaseChannel,
25+
getUpdatePlatformDirectory,
26+
GitHubReleaseCdnProvider,
27+
} from './githubReleaseCdnProvider';
2228

2329
const { autoUpdater } = createRequire(import.meta.url)('electron-updater');
2430

@@ -56,25 +62,42 @@ export function update(win: Electron.BrowserWindow) {
5662
console.log('Current version:', autoUpdater.currentVersion.version);
5763
console.log('Update config path:', autoUpdater.getUpdateConfigPath?.());
5864
console.log('User data path (where config lives):', app.getPath('userData'));
59-
if (app.isPackaged) {
60-
autoUpdater.checkForUpdatesAndNotify();
65+
const platformDir = getUpdatePlatformDirectory(
66+
process.platform,
67+
process.arch
68+
);
69+
70+
if (!platformDir) {
71+
console.warn(
72+
`[AutoUpdater] Updates are not configured for ${process.platform}/${process.arch}`
73+
);
74+
return;
6175
}
76+
77+
const cdnBaseUrl =
78+
process.env.EIGENT_UPDATER_CDN_BASE_URL || DEFAULT_CDN_RELEASE_BASE_URL;
79+
const channel = getGitHubReleaseChannel(process.platform, process.arch);
6280
const feed = {
63-
provider: 'github',
81+
provider: 'custom' as const,
82+
updateProvider: GitHubReleaseCdnProvider,
6483
owner: 'eigent-ai',
6584
repo: 'eigent',
66-
releaseType: 'release',
67-
channel:
68-
process.platform === 'darwin'
69-
? process.arch === 'arm64'
70-
? 'latest-arm64'
71-
: 'latest-x64'
72-
: 'latest',
85+
releaseType: 'release' as const,
86+
channel,
87+
cdnBaseUrl,
88+
platformDir,
7389
};
7490

7591
autoUpdater.setFeedURL(feed);
92+
console.log('[AutoUpdater] setFeedURL:', feed);
93+
94+
if (app.isPackaged) {
95+
autoUpdater.checkForUpdatesAndNotify().catch((err: Error) => {
96+
console.log('[AutoUpdater] Initial update check failed:', err.message);
97+
});
98+
}
99+
76100
if (!app.isPackaged) {
77-
console.log('[DEV] setFeedURL:', feed);
78101
// In development, check for updates but don't fail if it errors
79102
autoUpdater.checkForUpdates().catch((err: Error) => {
80103
console.log(
@@ -147,10 +170,24 @@ function startDownload(
147170
callback: (error: Error | null, info: ProgressInfo | null) => void,
148171
complete: (event: UpdateDownloadedEvent) => void
149172
) {
173+
const nativeMacUpdater =
174+
process.platform === 'darwin'
175+
? ((autoUpdater as { nativeUpdater?: NodeJS.EventEmitter })
176+
.nativeUpdater ?? null)
177+
: null;
178+
150179
autoUpdater.on('download-progress', (info: ProgressInfo) =>
151180
callback(null, info)
152181
);
153182
autoUpdater.on('error', (error: Error) => callback(error, null));
154-
autoUpdater.on('update-downloaded', complete);
183+
184+
if (nativeMacUpdater) {
185+
nativeMacUpdater.once('update-downloaded', () =>
186+
complete({} as UpdateDownloadedEvent)
187+
);
188+
} else {
189+
autoUpdater.on('update-downloaded', complete);
190+
}
191+
155192
autoUpdater.downloadUpdate();
156193
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
2+
// Licensed under the Apache License, Version 2.0 (the "License");
3+
// you may not use this file except in compliance with the License.
4+
// You may obtain a copy of the License at
5+
//
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
14+
15+
import {
16+
buildVersionedReleaseBaseUrl,
17+
getGitHubReleaseChannel,
18+
getUpdatePlatformDirectory,
19+
normalizeCdnReleaseBaseUrl,
20+
} from '../../../electron/main/githubReleaseCdnProvider';
21+
22+
describe('githubReleaseCdnProvider', () => {
23+
it('maps supported platforms to the expected release directories', () => {
24+
expect(getUpdatePlatformDirectory('darwin', 'arm64')).toBe('mac-arm64');
25+
expect(getUpdatePlatformDirectory('darwin', 'x64')).toBe('mac-intel');
26+
expect(getUpdatePlatformDirectory('win32', 'x64')).toBe('win-x64');
27+
expect(getUpdatePlatformDirectory('linux', 'x64')).toBe('linux-x64');
28+
});
29+
30+
it('returns null for unsupported platform and architecture combinations', () => {
31+
expect(getUpdatePlatformDirectory('darwin', 'ia32')).toBeNull();
32+
expect(getUpdatePlatformDirectory('win32', 'arm64')).toBeNull();
33+
expect(getUpdatePlatformDirectory('linux', 'arm64')).toBeNull();
34+
expect(getUpdatePlatformDirectory('freebsd', 'x64')).toBeNull();
35+
});
36+
37+
it('normalizes the CDN base URL before building versioned release paths', () => {
38+
expect(
39+
normalizeCdnReleaseBaseUrl('https://cdn.eigent.ai/releases///')
40+
).toBe('https://cdn.eigent.ai/releases');
41+
});
42+
43+
it('builds versioned CDN URLs for updater downloads', () => {
44+
expect(
45+
buildVersionedReleaseBaseUrl(
46+
'https://cdn.eigent.ai/releases/',
47+
'0.0.90',
48+
'mac-arm64'
49+
)
50+
).toBe('https://cdn.eigent.ai/releases/v0.0.90/mac-arm64/');
51+
52+
expect(
53+
buildVersionedReleaseBaseUrl(
54+
'https://cdn.eigent.ai/releases',
55+
'0.0.90',
56+
'win-x64'
57+
)
58+
).toBe('https://cdn.eigent.ai/releases/v0.0.90/win-x64/');
59+
});
60+
61+
it('maps mac builds to the GitHub release channels used in CI', () => {
62+
expect(getGitHubReleaseChannel('darwin', 'arm64')).toBe('latest-arm64');
63+
expect(getGitHubReleaseChannel('darwin', 'x64')).toBe('latest-x64');
64+
expect(getGitHubReleaseChannel('win32', 'x64')).toBe('latest');
65+
expect(getGitHubReleaseChannel('linux', 'x64')).toBe('latest');
66+
});
67+
});

0 commit comments

Comments
 (0)