diff --git a/src/main/updater-fallback.ts b/src/main/updater-fallback.ts index eab50f393..2fc0e94ac 100644 --- a/src/main/updater-fallback.ts +++ b/src/main/updater-fallback.ts @@ -97,6 +97,17 @@ export function isValidVersion(value: string): boolean { return parseVersion(value) !== null } +// Why: a user running a prerelease build (e.g. 1.3.17-rc.1) must stay on the +// RC channel for "Check for Updates" to resolve the next RC (1.3.17-rc.2). +// The default generic feed only advertises non-prerelease releases, so without +// this detection a prerelease user would be stuck unless they knew to +// Shift-click the menu item — effectively trapping them on the RC they +// installed. +export function isPrereleaseVersion(value: string): boolean { + const parsed = parseVersion(value) + return parsed !== null && parsed.prerelease.length > 0 +} + function compareIdentifiers(left: string, right: string): number { const leftNumeric = /^\d+$/.test(left) const rightNumeric = /^\d+$/.test(right) diff --git a/src/main/updater.fallback.test.ts b/src/main/updater.fallback.test.ts index 5b3de44a4..42d550007 100644 --- a/src/main/updater.fallback.test.ts +++ b/src/main/updater.fallback.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import { compareVersions } from './updater-fallback' +import { compareVersions, isPrereleaseVersion } from './updater-fallback' describe('compareVersions', () => { it('compares prerelease and build semver strings correctly', () => { @@ -9,3 +9,19 @@ describe('compareVersions', () => { expect(compareVersions('v1.0.70-beta.2', '1.0.70-beta.1')).toBeGreaterThan(0) }) }) + +describe('isPrereleaseVersion', () => { + it('recognises RC, beta, and alpha variants', () => { + expect(isPrereleaseVersion('1.3.17-rc.1')).toBe(true) + expect(isPrereleaseVersion('v1.3.17-rc.2')).toBe(true) + expect(isPrereleaseVersion('1.0.0-beta.5')).toBe(true) + expect(isPrereleaseVersion('2.1.0-alpha')).toBe(true) + }) + + it('returns false for stable releases and unparseable values', () => { + expect(isPrereleaseVersion('1.3.17')).toBe(false) + expect(isPrereleaseVersion('v1.3.17')).toBe(false) + expect(isPrereleaseVersion('1.3.17+build.5')).toBe(false) + expect(isPrereleaseVersion('not-a-version')).toBe(false) + }) +}) diff --git a/src/main/updater.test.ts b/src/main/updater.test.ts index ec3ceedcc..995d75ac8 100644 --- a/src/main/updater.test.ts +++ b/src/main/updater.test.ts @@ -750,4 +750,43 @@ describe('updater', () => { expect((autoUpdaterMock as Record).verifyUpdateCodeSignature).toBeUndefined() }) + + // Why: a user running an RC (e.g. 1.3.17-rc.1) must stay on the RC channel + // so the default "Check for Updates" path can find the next RC. The generic + // /releases/latest/download/ feed only exposes non-prerelease releases. + it('auto-enables the RC channel at setup when running a prerelease build', async () => { + appMock.getVersion.mockReturnValue('1.3.17-rc.1') + autoUpdaterMock.checkForUpdates.mockResolvedValue(undefined) + + const { setupAutoUpdater } = await import('./updater') + + const mainWindow = { webContents: { send: vi.fn() } } + setupAutoUpdater(mainWindow as never, { getLastUpdateCheckAt: () => Date.now() }) + + expect(autoUpdaterMock.allowPrerelease).toBe(true) + expect(autoUpdaterMock.setFeedURL).toHaveBeenCalledWith({ + provider: 'github', + owner: 'stablyai', + repo: 'orca' + }) + expect(autoUpdaterMock.setFeedURL).not.toHaveBeenCalledWith( + expect.objectContaining({ provider: 'generic' }) + ) + }) + + it('keeps the generic feed at setup when running a stable release', async () => { + appMock.getVersion.mockReturnValue('1.3.17') + autoUpdaterMock.checkForUpdates.mockResolvedValue(undefined) + + const { setupAutoUpdater } = await import('./updater') + + const mainWindow = { webContents: { send: vi.fn() } } + setupAutoUpdater(mainWindow as never, { getLastUpdateCheckAt: () => Date.now() }) + + expect(autoUpdaterMock.allowPrerelease).not.toBe(true) + expect(autoUpdaterMock.setFeedURL).toHaveBeenCalledWith({ + provider: 'generic', + url: 'https://github.com/stablyai/orca/releases/latest/download' + }) + }) }) diff --git a/src/main/updater.ts b/src/main/updater.ts index 60a4f9abf..21551e6c4 100644 --- a/src/main/updater.ts +++ b/src/main/updater.ts @@ -11,7 +11,12 @@ import { markMacQuitAndInstallInFlight } from './updater-mac-install' import { registerAutoUpdaterHandlers } from './updater-events' -import { compareVersions, isBenignCheckFailure, statusesEqual } from './updater-fallback' +import { + compareVersions, + isBenignCheckFailure, + isPrereleaseVersion, + statusesEqual +} from './updater-fallback' import { fetchNudge, shouldApplyNudge } from './updater-nudge' const AUTO_UPDATE_CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000 @@ -476,15 +481,24 @@ export function setupAutoUpdater( ;(autoUpdater as NsisUpdater).verifyUpdateCodeSignature = () => Promise.resolve(null) } - // Use the generic provider with GitHub's /releases/latest/download/ URL so - // electron-updater always fetches the manifest (latest-mac.yml, latest.yml, - // latest-linux.yml) from the latest non-prerelease release. This sidesteps - // the broken /releases/latest API endpoint (returns 406) and automatically - // excludes RC/prerelease versions without client-side filtering. - autoUpdater.setFeedURL({ - provider: 'generic', - url: 'https://github.com/stablyai/orca/releases/latest/download' - }) + // Why: a user already running a prerelease (e.g. 1.3.17-rc.1) must stay on + // the RC channel so "Check for Updates" can find the next RC (1.3.17-rc.2). + // The generic /releases/latest/download/ feed only advertises non-prerelease + // releases, so without this they'd never see the follow-up RC. Users on a + // stable release keep the generic feed and must Shift-click to opt in. + if (isPrereleaseVersion(app.getVersion())) { + enableIncludePrerelease() + } else { + // Use the generic provider with GitHub's /releases/latest/download/ URL so + // electron-updater always fetches the manifest (latest-mac.yml, latest.yml, + // latest-linux.yml) from the latest non-prerelease release. This sidesteps + // the broken /releases/latest API endpoint (returns 406) and automatically + // excludes RC/prerelease versions without client-side filtering. + autoUpdater.setFeedURL({ + provider: 'generic', + url: 'https://github.com/stablyai/orca/releases/latest/download' + }) + } if (autoUpdaterInitialized) { return