@@ -22,9 +22,11 @@ import type { SentryContext } from "../../context.js";
2222import {
2323 determineInstallDir ,
2424 isDowngrade ,
25+ isNightlyVersion ,
2526 LEGACY_INSTALL_SUBDIR ,
2627 releaseLock ,
2728 samePath ,
29+ type UpgradeSource ,
2830} from "../../lib/binary.js" ;
2931import { buildCommand } from "../../lib/command.js" ;
3032import { CLI_VERSION } from "../../lib/constants.js" ;
@@ -34,7 +36,7 @@ import {
3436 setReleaseChannel ,
3537} from "../../lib/db/release-channel.js" ;
3638import { getVersionCheckInfo } from "../../lib/db/version-check.js" ;
37- import { UpgradeError } from "../../lib/errors.js" ;
39+ import { UpgradeError , UpgradeTransportError } from "../../lib/errors.js" ;
3840import { formatUpgradeResult } from "../../lib/formatters/human.js" ;
3941import { formatBytes } from "../../lib/formatters/numbers.js" ;
4042import { CommandOutput } from "../../lib/formatters/output.js" ;
@@ -54,6 +56,8 @@ import {
5456 NIGHTLY_TAG ,
5557 type OfflineMode ,
5658 parseInstallationMethod ,
59+ resolveExistingUpgradeVersion ,
60+ resolveLatestUpgradeVersion ,
5761 VERSION_PREFIX_REGEX ,
5862 versionExists ,
5963} from "../../lib/upgrade.js" ;
@@ -172,8 +176,13 @@ async function resolveTargetWithFallback(opts: {
172176 * clearing the version cache before the offline path can read it). */
173177 persistChannelFn : ( ) => void ;
174178} ) : Promise <
175- | { kind : "target" ; target : string ; offline : OfflineMode }
176- | { kind : "done" ; result : UpgradeResult }
179+ | {
180+ kind : "target" ;
181+ target : string ;
182+ offline : OfflineMode ;
183+ source ?: UpgradeSource ;
184+ }
185+ | { kind : "done" ; result : UpgradeResult ; source ?: UpgradeSource }
177186> {
178187 const { resolveOpts, versionArg, offline, method, persistChannelFn } = opts ;
179188
@@ -203,15 +212,17 @@ async function resolveTargetWithFallback(opts: {
203212 if ( resolved . kind === "done" ) {
204213 return resolved ;
205214 }
206- return { kind : "target" , target : resolved . target , offline : false } ;
215+ return {
216+ kind : "target" ,
217+ target : resolved . target ,
218+ offline : false ,
219+ source : resolved . source ,
220+ } ;
207221 } catch ( error ) {
208222 // Automatic offline fallback: only for curl-installed binaries (package
209223 // managers need the network for the actual install, not just version
210224 // discovery), and only for network errors (not version_not_found etc.)
211- if (
212- method !== "curl" ||
213- ! ( error instanceof UpgradeError && error . reason === "network_error" )
214- ) {
225+ if ( method !== "curl" || ! ( error instanceof UpgradeTransportError ) ) {
215226 throw error ;
216227 }
217228 try {
@@ -233,15 +244,17 @@ async function resolveTargetWithFallback(opts: {
233244function validateMethod (
234245 method : InstallationMethod ,
235246 versionArg : string | undefined ,
236- channel : ReleaseChannel ,
237247 offline : boolean
238248) : void {
239249 if ( method === "unknown" ) {
240250 throw new UpgradeError ( "unknown_method" ) ;
241251 }
242252 // Homebrew manages versioning through the formula — pinning a specific
243253 // stable version is not supported via this command.
244- if ( method === "brew" && versionArg && channel === "stable" ) {
254+ const pinnedVersion = CHANNEL_VERSIONS . has ( versionArg ?? "" )
255+ ? undefined
256+ : versionArg ?. replace ( VERSION_PREFIX_REGEX , "" ) ;
257+ if ( method === "brew" && pinnedVersion && ! isNightlyVersion ( pinnedVersion ) ) {
245258 throw new UpgradeError (
246259 "unsupported_operation" ,
247260 "Homebrew does not support installing a specific version. Run 'brew upgrade getsentry/tools/sentry' to upgrade to the latest formula version."
@@ -257,6 +270,10 @@ function validateMethod(
257270 }
258271}
259272
273+ function getArtifactChannel ( target : string ) : ReleaseChannel {
274+ return isNightlyVersion ( target ) ? "nightly" : "stable" ;
275+ }
276+
260277type ResolveTargetOptions = {
261278 method : InstallationMethod ;
262279 channel : ReleaseChannel ;
@@ -273,8 +290,28 @@ type ResolveTargetOptions = {
273290 * (check-only mode, or already up to date)
274291 */
275292type ResolveResult =
276- | { kind : "target" ; target : string }
277- | { kind : "done" ; result : UpgradeResult } ;
293+ | { kind : "target" ; target : string ; source ?: UpgradeSource }
294+ | { kind : "done" ; result : UpgradeResult ; source ?: UpgradeSource } ;
295+
296+ async function resolvePinnedVersion (
297+ lookupMethod : InstallationMethod ,
298+ target : string
299+ ) : Promise < UpgradeSource | undefined > {
300+ if ( lookupMethod !== "curl" ) {
301+ if ( ! ( await versionExists ( lookupMethod , target ) ) ) {
302+ throw new UpgradeError (
303+ "version_not_found" ,
304+ `Version ${ target } not found`
305+ ) ;
306+ }
307+ return ;
308+ }
309+ const resolved = await resolveExistingUpgradeVersion ( target ) ;
310+ if ( ! resolved ) {
311+ throw new UpgradeError ( "version_not_found" , `Version ${ target } not found` ) ;
312+ }
313+ return resolved . source ;
314+ }
278315
279316/**
280317 * Resolve the target version and handle check-only mode.
@@ -286,52 +323,66 @@ async function resolveTargetVersion(
286323 opts : ResolveTargetOptions
287324) : Promise < ResolveResult > {
288325 const { method, channel, versionArg, channelChanged, flags } = opts ;
289- const latest = await fetchLatestVersion ( method , channel ) ;
290- const target = versionArg ?. replace ( VERSION_PREFIX_REGEX , "" ) ?? latest ;
326+ const standalone =
327+ channel === "nightly" || method === "curl" || method === "brew" ;
328+ const pinnedTarget =
329+ versionArg && ! CHANNEL_VERSIONS . has ( versionArg )
330+ ? versionArg . replace ( VERSION_PREFIX_REGEX , "" )
331+ : undefined ;
332+ let source : UpgradeSource | undefined ;
333+
334+ if ( pinnedTarget ) {
335+ const lookupMethod = isNightlyVersion ( pinnedTarget ) ? "curl" : method ;
336+ source = await resolvePinnedVersion ( lookupMethod , pinnedTarget ) ;
337+ }
338+
339+ const latestResolution =
340+ pinnedTarget === undefined && standalone
341+ ? await resolveLatestUpgradeVersion ( channel )
342+ : undefined ;
343+ const latest =
344+ pinnedTarget ??
345+ latestResolution ?. version ??
346+ ( await fetchLatestVersion ( method , channel ) ) ;
347+ const resolvedTarget = pinnedTarget ?? latest ;
348+ source ??= latestResolution ?. source ;
291349
292350 log . debug ( `Channel: ${ channel } ` ) ;
293351 log . debug ( `Latest version: ${ latest } ` ) ;
294352 if ( versionArg ) {
295- log . debug ( `Target version: ${ target } ` ) ;
353+ log . debug ( `Target version: ${ resolvedTarget } ` ) ;
296354 }
297355
298356 if ( flags . check ) {
299357 return {
300358 kind : "done" ,
301- result : buildCheckResult ( { target, versionArg, method, channel, flags } ) ,
359+ result : buildCheckResult ( {
360+ target : resolvedTarget ,
361+ versionArg,
362+ method,
363+ channel,
364+ flags,
365+ } ) ,
366+ source,
302367 } ;
303368 }
304369
305370 // Skip if already on target — unless forced or switching channels
306- if ( CLI_VERSION === target && ! flags . force && ! channelChanged ) {
371+ if ( CLI_VERSION === resolvedTarget && ! flags . force && ! channelChanged ) {
307372 return {
308373 kind : "done" ,
309374 result : {
310375 action : "up-to-date" ,
311376 currentVersion : CLI_VERSION ,
312- targetVersion : target ,
377+ targetVersion : resolvedTarget ,
313378 channel,
314379 method,
315380 forced : false ,
316381 } ,
317382 } ;
318383 }
319384
320- // Validate that a specific pinned version actually exists.
321- // Nightly builds are GitHub-only, so always use curl (GitHub) lookup for
322- // nightly channel regardless of the current install method.
323- if ( versionArg && ! CHANNEL_VERSIONS . has ( versionArg ) ) {
324- const lookupMethod = channel === "nightly" ? "curl" : method ;
325- const exists = await versionExists ( lookupMethod , target ) ;
326- if ( ! exists ) {
327- throw new UpgradeError (
328- "version_not_found" ,
329- `Version ${ target } not found`
330- ) ;
331- }
332- }
333-
334- return { kind : "target" , target } ;
385+ return { kind : "target" , target : resolvedTarget , source } ;
335386}
336387
337388/**
@@ -617,6 +668,7 @@ async function executeStandardUpgrade(opts: {
617668 offline ?: OfflineMode ;
618669 json ?: boolean ;
619670 noAgentSkills : boolean ;
671+ source ?: UpgradeSource ;
620672} ) : Promise < void > {
621673 const {
622674 method,
@@ -629,6 +681,7 @@ async function executeStandardUpgrade(opts: {
629681 offline,
630682 json,
631683 noAgentSkills,
684+ source,
632685 } = opts ;
633686
634687 // Use the rolling "nightly" tag only when upgrading to latest nightly
@@ -639,7 +692,7 @@ async function executeStandardUpgrade(opts: {
639692 const downloadResult = await withProgress (
640693 { message : `Downloading ${ target } ...` , json } ,
641694 async ( setMessage ) =>
642- executeUpgrade ( method , target , downloadTag , offline , setMessage )
695+ executeUpgrade ( method , target , downloadTag , offline , setMessage , source )
643696 ) ;
644697
645698 if ( downloadResult ?. patchBytes ) {
@@ -713,8 +766,11 @@ async function migrateToStandaloneForNightly(opts: {
713766 versionArg : string | undefined ;
714767 noAgentSkills : boolean ;
715768 json ?: boolean ;
769+ source ?: UpgradeSource ;
770+ channel : ReleaseChannel ;
716771} ) : Promise < string [ ] > {
717- const { method, target, versionArg, noAgentSkills, json } = opts ;
772+ const { method, target, versionArg, noAgentSkills, json, source, channel } =
773+ opts ;
718774 log . info ( "Nightly builds are only available as standalone binaries." ) ;
719775 log . info ( "Migrating to standalone installation..." ) ;
720776
@@ -724,7 +780,7 @@ async function migrateToStandaloneForNightly(opts: {
724780 const downloadResult = await withProgress (
725781 { message : `Downloading ${ target } ...` , json } ,
726782 async ( setMessage ) =>
727- executeUpgrade ( "curl" , target , downloadTag , undefined , setMessage )
783+ executeUpgrade ( "curl" , target , downloadTag , undefined , setMessage , source )
728784 ) ;
729785
730786 if ( downloadResult ?. patchBytes ) {
@@ -746,7 +802,7 @@ async function migrateToStandaloneForNightly(opts: {
746802 await runSetupOnNewBinary ( {
747803 binaryPath : downloadResult . tempBinaryPath ,
748804 method : "curl" ,
749- channel : "nightly" ,
805+ channel,
750806 install : true ,
751807 installDir,
752808 ensureAuthScopes : ! json ,
@@ -796,7 +852,7 @@ async function resolveContext(
796852 const channelChanged = channel !== currentChannel ;
797853
798854 const method = flags . method ?? ( await detectInstallationMethod ( ) ) ;
799- validateMethod ( method , versionArg , channel , flags . offline ) ;
855+ validateMethod ( method , versionArg , flags . offline ) ;
800856 return { channel, versionArg, channelChanged, method } ;
801857}
802858
@@ -822,19 +878,22 @@ function persistChannel(
822878 * Returns a promise that resolves to the changelog or undefined. Never
823879 * throws — errors are swallowed so the upgrade is not blocked.
824880 */
825- function startChangelogFetch (
826- channel : ReleaseChannel ,
827- currentVersion : string ,
828- targetVersion : string ,
829- offline : OfflineMode
830- ) : Promise < ChangelogSummary | undefined > {
881+ function startChangelogFetch ( options : {
882+ channel : ReleaseChannel ;
883+ currentVersion : string ;
884+ targetVersion : string ;
885+ offline : OfflineMode ;
886+ source ?: UpgradeSource ;
887+ } ) : Promise < ChangelogSummary | undefined > {
888+ const { channel, currentVersion, targetVersion, offline, source } = options ;
831889 if ( offline || currentVersion === targetVersion ) {
832890 return Promise . resolve ( undefined ) ;
833891 }
834892 return fetchChangelog ( {
835893 channel,
836894 fromVersion : currentVersion ,
837895 toVersion : targetVersion ,
896+ source,
838897 } )
839898 . then ( ( result ) => result ?? undefined )
840899 . catch ( ( ) => undefined as undefined ) ;
@@ -955,25 +1014,27 @@ export const upgradeCommand = buildCommand({
9551014 result . action === "checked" &&
9561015 result . currentVersion !== result . targetVersion
9571016 ) {
958- result . changelog = await startChangelogFetch (
959- channel ,
960- CLI_VERSION ,
961- result . targetVersion ,
962- false
963- ) ;
1017+ result . changelog = await startChangelogFetch ( {
1018+ channel : getArtifactChannel ( result . targetVersion ) ,
1019+ currentVersion : CLI_VERSION ,
1020+ targetVersion : result . targetVersion ,
1021+ offline : false ,
1022+ source : resolved . source ,
1023+ } ) ;
9641024 }
9651025 return yield new CommandOutput ( result ) ;
9661026 }
9671027
968- const { target, offline } = resolved ;
1028+ const { target, offline, source } = resolved ;
9691029
9701030 // Start changelog fetch early — it runs in parallel with the download.
971- const changelogPromise = startChangelogFetch (
972- channel ,
973- CLI_VERSION ,
974- target ,
975- offline
976- ) ;
1031+ const changelogPromise = startChangelogFetch ( {
1032+ channel : getArtifactChannel ( target ) ,
1033+ currentVersion : CLI_VERSION ,
1034+ targetVersion : target ,
1035+ offline,
1036+ source,
1037+ } ) ;
9771038
9781039 // --check with offline fallback: resolveTargetWithFallback returns
9791040 // kind: "target" for offline check, so guard against actual upgrade.
@@ -1008,7 +1069,7 @@ export const upgradeCommand = buildCommand({
10081069
10091070 // Perform the actual upgrade
10101071 let warnings : string [ ] | undefined ;
1011- if ( channel === "nightly" && method !== "curl" ) {
1072+ if ( isNightlyVersion ( target ) && method !== "curl" ) {
10121073 // Nightly is GitHub-only. If the current install method is not curl,
10131074 // migrate to a standalone binary — the migration handles setup internally.
10141075 warnings = await migrateToStandaloneForNightly ( {
@@ -1017,6 +1078,8 @@ export const upgradeCommand = buildCommand({
10171078 versionArg,
10181079 noAgentSkills : flags [ "no-agent-skills" ] ,
10191080 json : flags . json ,
1081+ source,
1082+ channel,
10201083 } ) ;
10211084 } else {
10221085 await executeStandardUpgrade ( {
@@ -1030,6 +1093,7 @@ export const upgradeCommand = buildCommand({
10301093 offline,
10311094 json : flags . json ,
10321095 noAgentSkills : flags [ "no-agent-skills" ] ,
1096+ source,
10331097 } ) ;
10341098 }
10351099
0 commit comments