fix(updater): keep prerelease users on the RC channel (#1053)

Detect when the running build is itself a prerelease (e.g. 1.3.17-rc.1)
and auto-enable the RC feed in setupAutoUpdater. Without this, the
default generic /releases/latest/download/ feed only advertises stable
releases, so "Check for Updates" from a prerelease build could never
find the next RC (e.g. 1.3.17-rc.2) unless the user knew to Shift-click.
This commit is contained in:
Neil 2026-04-24 14:36:22 -07:00 committed by GitHub
parent 749ba42cb2
commit f0c96bd2bf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 91 additions and 11 deletions

View File

@ -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)

View File

@ -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)
})
})

View File

@ -750,4 +750,43 @@ describe('updater', () => {
expect((autoUpdaterMock as Record<string, unknown>).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'
})
})
})

View File

@ -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