feat(updater): mark the hourly channel macOS-only in the picker (#11708)
The hourly workflow builds only macOS artifacts, so on Linux and Windows the channel had nothing to install while still looking selectable. Adds isChannelSupportedOnPlatform in the shared module so the picker and the main-process check cannot drift. The segment is disabled with an Apple glyph and a tooltip rather than hidden, so a dev who has heard of the channel sees that it exists and why it is unavailable instead of not finding it. A persisted 'hourly' override arriving off-macOS (settings sync, or a profile carried from a Mac) now falls back to stable rather than rendering a selected segment the user cannot act on. checkForPinnedBuild rejects the channel too, since IPC is reachable regardless of what the renderer disables.
This commit is contained in:
parent
aa6f945001
commit
94cf2f1422
|
|
@ -45,7 +45,11 @@ import {
|
|||
} from './serve-update-handoff'
|
||||
import type { LocalBuildFeed } from './local-builds/local-build-feed-server'
|
||||
import { listReleaseBuilds, resolveTargetBuild } from './updater-release-builds'
|
||||
import type { ReleaseBuild, ReleaseChannel } from '../shared/release-channel'
|
||||
import {
|
||||
isChannelSupportedOnPlatform,
|
||||
type ReleaseBuild,
|
||||
type ReleaseChannel
|
||||
} from '../shared/release-channel'
|
||||
|
||||
type CheckFailureSource = 'event' | 'promise' | 'fallback-promise'
|
||||
type MissingManifestPrereleaseFallbackResult = { userInitiated: boolean }
|
||||
|
|
@ -1524,6 +1528,16 @@ async function checkForPinnedBuild(channel: ReleaseChannel, tag: string): Promis
|
|||
sendStatus({ state: 'not-available', userInitiated: true })
|
||||
return
|
||||
}
|
||||
// Why here as well as in the picker: the renderer disables the option, but IPC
|
||||
// is reachable regardless, and there is no artifact to install off-macOS.
|
||||
if (!isChannelSupportedOnPlatform(channel, process.platform)) {
|
||||
sendStatus({
|
||||
state: 'error',
|
||||
message: 'Hourly builds are produced only for macOS.',
|
||||
userInitiated: true
|
||||
})
|
||||
return
|
||||
}
|
||||
if (currentStatus.state === 'checking' || currentStatus.state === 'downloading') {
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import type React from 'react'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { AlertTriangle, Loader2, RefreshCw } from 'lucide-react'
|
||||
import { AlertTriangle, Apple, Loader2, RefreshCw } from 'lucide-react'
|
||||
import { toast } from 'sonner'
|
||||
import { useAppStore } from '../../store'
|
||||
import { Button } from '../ui/button'
|
||||
|
|
@ -8,9 +8,11 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '.
|
|||
import { SettingsSegmentedControl, SettingsSubsectionHeader } from './SettingsFormControls'
|
||||
import { Badge } from '../ui/badge'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { getShortcutPlatform } from '@/lib/shortcut-platform'
|
||||
import {
|
||||
RELEASE_CHANNELS,
|
||||
getVersionChannel,
|
||||
isChannelSupportedOnPlatform,
|
||||
parseHourlyVersionStamp,
|
||||
type ReleaseBuild,
|
||||
type ReleaseChannel
|
||||
|
|
@ -25,7 +27,7 @@ const CHANNEL_LABELS: Record<ReleaseChannel, string> = {
|
|||
const CHANNEL_DESCRIPTIONS: Record<ReleaseChannel, string> = {
|
||||
stable: 'Shipped releases. What everyone else is running.',
|
||||
rc: 'Release candidates cut ahead of each stable.',
|
||||
hourly: 'Unvetted macOS builds from main, built every hour. No tests, no notarization.'
|
||||
hourly: 'macOS only. Unvetted builds from main, built every hour. No tests, no notarization.'
|
||||
}
|
||||
|
||||
function formatBuildLabel(build: ReleaseBuild): string {
|
||||
|
|
@ -54,8 +56,15 @@ export function ReleaseChannelSection(): React.JSX.Element {
|
|||
const [loading, setLoading] = useState(false)
|
||||
const [selectedTag, setSelectedTag] = useState<string | null>(null)
|
||||
|
||||
const platform = getShortcutPlatform()
|
||||
const runningChannel = appVersion ? getVersionChannel(appVersion) : null
|
||||
const activeChannel = releaseChannelOverride ?? runningChannel ?? 'stable'
|
||||
const requestedChannel = releaseChannelOverride ?? runningChannel ?? 'stable'
|
||||
// Why: a persisted 'hourly' can arrive on Linux/Windows — settings sync, or a
|
||||
// profile carried over from a Mac. Fall back rather than rendering a selected
|
||||
// segment the user cannot act on and a build list that can never install.
|
||||
const activeChannel = isChannelSupportedOnPlatform(requestedChannel, platform)
|
||||
? requestedChannel
|
||||
: 'stable'
|
||||
const busy = updateStatus.state === 'checking' || updateStatus.state === 'downloading'
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -170,10 +179,36 @@ export function ReleaseChannelSection(): React.JSX.Element {
|
|||
'auto.components.settings.ReleaseChannelSection.channelAriaLabel',
|
||||
'Update channel'
|
||||
)}
|
||||
options={RELEASE_CHANNELS.map((channel) => ({
|
||||
value: channel,
|
||||
label: CHANNEL_LABELS[channel]
|
||||
}))}
|
||||
// Why disabled rather than hidden: a Linux/Windows dev who has heard
|
||||
// about the hourly channel should see that it exists and why it is
|
||||
// unavailable, instead of silently not finding it.
|
||||
options={RELEASE_CHANNELS.map((channel) => {
|
||||
const supported = isChannelSupportedOnPlatform(channel, platform)
|
||||
return {
|
||||
value: channel,
|
||||
label: supported ? (
|
||||
CHANNEL_LABELS[channel]
|
||||
) : (
|
||||
<span className="inline-flex items-center gap-1">
|
||||
{CHANNEL_LABELS[channel]}
|
||||
<Apple className="size-3" aria-hidden="true" />
|
||||
</span>
|
||||
),
|
||||
disabled: !supported,
|
||||
ariaLabel: supported
|
||||
? undefined
|
||||
: translate(
|
||||
'auto.components.settings.ReleaseChannelSection.hourlyMacOnlyAria',
|
||||
'Hourly (macOS only)'
|
||||
),
|
||||
tooltip: supported
|
||||
? undefined
|
||||
: translate(
|
||||
'auto.components.settings.ReleaseChannelSection.hourlyMacOnly',
|
||||
'Hourly builds are produced only for macOS. Linux and Windows stay on Stable or RC.'
|
||||
)
|
||||
}
|
||||
})}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">{CHANNEL_DESCRIPTIONS[activeChannel]}</p>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -10089,7 +10089,9 @@
|
|||
"switchTo": "Switch to build",
|
||||
"alreadyRunning": "This is the build you are running.",
|
||||
"willSwitch": "{{value0}} → {{value1}}",
|
||||
"webUnavailable": "Switching builds is only available in the desktop app."
|
||||
"webUnavailable": "Switching builds is only available in the desktop app.",
|
||||
"hourlyMacOnlyAria": "Hourly (macOS only)",
|
||||
"hourlyMacOnly": "Hourly builds are produced only for macOS. Linux and Windows stay on Stable or RC."
|
||||
}
|
||||
},
|
||||
"right": {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import {
|
|||
formatHourlyVersion,
|
||||
getReleaseRepoForChannel,
|
||||
getVersionChannel,
|
||||
isChannelSupportedOnPlatform,
|
||||
isHourlyVersion,
|
||||
isReleaseChannel,
|
||||
parseHourlyVersionStamp,
|
||||
|
|
@ -57,6 +58,22 @@ describe('release channel', () => {
|
|||
)
|
||||
})
|
||||
|
||||
// Why: the hourly workflow is macOS-only, so the channel has no artifact to
|
||||
// offer elsewhere. Both the picker and the main-process check read this, so a
|
||||
// regression here would silently re-expose an uninstallable channel.
|
||||
it('offers hourly only on macOS', () => {
|
||||
expect(isChannelSupportedOnPlatform('hourly', 'darwin')).toBe(true)
|
||||
expect(isChannelSupportedOnPlatform('hourly', 'linux')).toBe(false)
|
||||
expect(isChannelSupportedOnPlatform('hourly', 'win32')).toBe(false)
|
||||
})
|
||||
|
||||
it('offers stable and rc on every platform', () => {
|
||||
for (const platform of ['darwin', 'linux', 'win32'] as const) {
|
||||
expect(isChannelSupportedOnPlatform('stable', platform)).toBe(true)
|
||||
expect(isChannelSupportedOnPlatform('rc', platform)).toBe(true)
|
||||
}
|
||||
})
|
||||
|
||||
it('accepts only known channels', () => {
|
||||
expect(isReleaseChannel('hourly')).toBe(true)
|
||||
expect(isReleaseChannel('stable')).toBe(true)
|
||||
|
|
|
|||
|
|
@ -16,6 +16,18 @@ export function isReleaseChannel(value: unknown): value is ReleaseChannel {
|
|||
return typeof value === 'string' && RELEASE_CHANNELS.includes(value as ReleaseChannel)
|
||||
}
|
||||
|
||||
/**
|
||||
* Hourly builds are produced only by the macOS workflow, so the channel has
|
||||
* nothing to offer elsewhere. Shared so the picker, the main-process check, and
|
||||
* any future surface cannot drift on where it is available.
|
||||
*/
|
||||
export function isChannelSupportedOnPlatform(
|
||||
channel: ReleaseChannel,
|
||||
platform: NodeJS.Platform
|
||||
): boolean {
|
||||
return channel !== 'hourly' || platform === 'darwin'
|
||||
}
|
||||
|
||||
export function getReleaseRepoForChannel(channel: ReleaseChannel): string {
|
||||
return channel === 'hourly' ? HOURLY_RELEASE_REPO : MAIN_RELEASE_REPO
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue