@@ -30,6 +30,7 @@ import {
3030 setReleaseChannel ,
3131} from "../../lib/db/release-channel.js" ;
3232import { UpgradeError } from "../../lib/errors.js" ;
33+ import { formatUpgradeResult } from "../../lib/formatters/human.js" ;
3334import { logger } from "../../lib/logger.js" ;
3435import {
3536 detectInstallationMethod ,
@@ -48,6 +49,30 @@ const log = logger.withTag("cli.upgrade");
4849/** Special version strings that select a channel rather than a specific release. */
4950const CHANNEL_VERSIONS = new Set ( [ "nightly" , "stable" ] ) ;
5051
52+ /**
53+ * Structured result of the upgrade command.
54+ *
55+ * Returned as `{ data: UpgradeResult }` and rendered via the output config.
56+ * In JSON mode the object is serialized as-is; in human mode it's passed to
57+ * {@link formatUpgradeResult}.
58+ */
59+ export type UpgradeResult = {
60+ /** What action was taken */
61+ action : "upgraded" | "downgraded" | "up-to-date" | "checked" | "channel-only" ;
62+ /** Current CLI version before upgrade */
63+ currentVersion : string ;
64+ /** Target version (the version we upgraded/downgraded to, or the latest available) */
65+ targetVersion : string ;
66+ /** Release channel */
67+ channel : "stable" | "nightly" ;
68+ /** Installation method used */
69+ method : string ;
70+ /** Whether the user forced the upgrade */
71+ forced : boolean ;
72+ /** Warnings to display (e.g., PATH shadowing from old package manager install) */
73+ warnings ?: string [ ] ;
74+ } ;
75+
5176type UpgradeFlags = {
5277 readonly check : boolean ;
5378 readonly force : boolean ;
@@ -90,15 +115,26 @@ type ResolveTargetOptions = {
90115 flags : UpgradeFlags ;
91116} ;
92117
118+ /**
119+ * Result of resolving the target version.
120+ *
121+ * - `target`: the version string to upgrade/downgrade to (proceed with upgrade)
122+ * - `UpgradeResult`: structured result when no upgrade should proceed
123+ * (check-only mode, already up to date, or channel-only switch)
124+ */
125+ type ResolveResult =
126+ | { kind : "target" ; target : string }
127+ | { kind : "done" ; result : UpgradeResult } ;
128+
93129/**
94130 * Resolve the target version and handle check-only mode.
95131 *
96- * @returns The target version string, or null if no upgrade should proceed
97- * (check-only mode, already up to date without --force, or channel unchanged) .
132+ * @returns A `ResolveResult` indicating whether to proceed with the upgrade
133+ * or return a completed result immediately .
98134 */
99135async function resolveTargetVersion (
100136 opts : ResolveTargetOptions
101- ) : Promise < string | null > {
137+ ) : Promise < ResolveResult > {
102138 const { method, channel, versionArg, channelChanged, flags } = opts ;
103139 const latest = await fetchLatestVersion ( method , channel ) ;
104140 const target = versionArg ?. replace ( VERSION_PREFIX_REGEX , "" ) ?? latest ;
@@ -110,13 +146,25 @@ async function resolveTargetVersion(
110146 }
111147
112148 if ( flags . check ) {
113- return handleCheckMode ( target , versionArg ) ;
149+ return {
150+ kind : "done" ,
151+ result : buildCheckResult ( { target, versionArg, method, channel, flags } ) ,
152+ } ;
114153 }
115154
116155 // Skip if already on target — unless forced or switching channels
117156 if ( CLI_VERSION === target && ! flags . force && ! channelChanged ) {
118- log . success ( "Already up to date." ) ;
119- return null ;
157+ return {
158+ kind : "done" ,
159+ result : {
160+ action : "up-to-date" ,
161+ currentVersion : CLI_VERSION ,
162+ targetVersion : target ,
163+ channel,
164+ method,
165+ forced : false ,
166+ } ,
167+ } ;
120168 }
121169
122170 // Validate that a specific pinned version actually exists.
@@ -133,23 +181,39 @@ async function resolveTargetVersion(
133181 }
134182 }
135183
136- return target ;
184+ return { kind : " target" , target } ;
137185}
138186
139187/**
140- * Print check-only output and return null (no upgrade to perform) .
188+ * Build the structured result for check-only mode .
141189 */
142- function handleCheckMode ( target : string , versionArg : string | undefined ) : null {
143- if ( CLI_VERSION === target ) {
144- log . success ( "You are already on the target version." ) ;
145- } else {
190+ function buildCheckResult ( opts : {
191+ target : string ;
192+ versionArg : string | undefined ;
193+ method : InstallationMethod ;
194+ channel : ReleaseChannel ;
195+ flags : UpgradeFlags ;
196+ } ) : UpgradeResult {
197+ const { target, versionArg, method, channel, flags } = opts ;
198+ const result : UpgradeResult = {
199+ action : "checked" ,
200+ currentVersion : CLI_VERSION ,
201+ targetVersion : target ,
202+ channel,
203+ method,
204+ forced : flags . force ,
205+ } ;
206+
207+ // When already on target, no update hint needed
208+ if ( CLI_VERSION !== target ) {
146209 const cmd =
147210 versionArg && ! CHANNEL_VERSIONS . has ( versionArg )
148211 ? `sentry cli upgrade ${ target } `
149212 : "sentry cli upgrade" ;
150- log . info ( `Run '${ cmd } ' to update.` ) ;
213+ result . warnings = [ `Run '${ cmd } ' to update.` ] ;
151214 }
152- return null ;
215+
216+ return result ;
153217}
154218
155219/**
@@ -272,17 +336,18 @@ async function executeStandardUpgrade(opts: {
272336 * 1. Download the nightly binary to a temp path
273337 * 2. Install it to `determineInstallDir()` (same logic as the curl installer)
274338 * 3. Run setup on the new binary to update completions, PATH, and metadata
275- * 4. Warn about the old package-manager installation that may still be in PATH
339+ * 4. Return warnings about the old package-manager installation that may still be in PATH
276340 *
277341 * @param versionArg - Specific version requested by the user, or undefined for
278342 * latest nightly. When a specific version is given, its release tag is used
279343 * instead of the rolling "nightly" tag so the correct binary is downloaded.
344+ * @returns Warnings about the old installation that may shadow the new one
280345 */
281346async function migrateToStandaloneForNightly (
282347 method : InstallationMethod ,
283348 target : string ,
284349 versionArg : string | undefined
285- ) : Promise < void > {
350+ ) : Promise < string [ ] > {
286351 log . info ( "Nightly builds are only available as standalone binaries." ) ;
287352 log . info ( "Migrating to standalone installation..." ) ;
288353
@@ -311,7 +376,7 @@ async function migrateToStandaloneForNightly(
311376 releaseLock ( downloadResult . lockPath ) ;
312377 }
313378
314- // Warn about the potentially shadowing old installation
379+ // Build warnings about the potentially shadowing old installation.
315380 // Note: install info is already recorded by the child `setup --install`
316381 // process, so no redundant setInstallInfo call is needed here.
317382 const uninstallHints : Record < string , string > = {
@@ -321,11 +386,15 @@ async function migrateToStandaloneForNightly(
321386 yarn : "yarn global remove sentry" ,
322387 brew : "brew uninstall getsentry/tools/sentry" ,
323388 } ;
389+ const warnings : string [ ] = [ ] ;
390+ warnings . push (
391+ `Your ${ method } -installed sentry may still appear earlier in PATH.`
392+ ) ;
324393 const hint = uninstallHints [ method ] ;
325- log . warn ( `Your ${ method } -installed sentry may still appear earlier in PATH.` ) ;
326394 if ( hint ) {
327- log . warn ( `Consider removing it: ${ hint } ` ) ;
395+ warnings . push ( `Consider removing it: ${ hint } ` ) ;
328396 }
397+ return warnings ;
329398}
330399
331400export const upgradeCommand = buildCommand ( {
@@ -348,6 +417,7 @@ export const upgradeCommand = buildCommand({
348417 " sentry cli upgrade --force # Force re-download even if up to date\n" +
349418 " sentry cli upgrade --method npm # Force using npm to upgrade" ,
350419 } ,
420+ output : { json : true , human : formatUpgradeResult } ,
351421 parameters : {
352422 positional : {
353423 kind : "tuple" ,
@@ -381,11 +451,7 @@ export const upgradeCommand = buildCommand({
381451 } ,
382452 } ,
383453 } ,
384- async func (
385- this : SentryContext ,
386- flags : UpgradeFlags ,
387- version ?: string
388- ) : Promise < void > {
454+ async func ( this : SentryContext , flags : UpgradeFlags , version ?: string ) {
389455 // Resolve effective channel and version from positional
390456 const { channel, versionArg } = resolveChannelAndVersion ( version ) ;
391457
@@ -419,27 +485,41 @@ export const upgradeCommand = buildCommand({
419485 log . info ( `Installation method: ${ method } ` ) ;
420486 log . info ( `Current version: ${ CLI_VERSION } ` ) ;
421487
422- const target = await resolveTargetVersion ( {
488+ const resolved = await resolveTargetVersion ( {
423489 method,
424490 channel,
425491 versionArg,
426492 channelChanged,
427493 flags,
428494 } ) ;
429- if ( ! target ) {
430- return ;
495+ if ( resolved . kind === "done" ) {
496+ return { data : resolved . result } ;
431497 }
432498
499+ const { target } = resolved ;
433500 const downgrade = isDowngrade ( CLI_VERSION , target ) ;
434501 log . info ( `${ downgrade ? "Downgrading" : "Upgrading" } to ${ target } ...` ) ;
435502
436503 // Nightly is GitHub-only. If the current install method is not curl,
437504 // migrate to a standalone binary first then return — the migration
438505 // handles setup internally.
439506 if ( channel === "nightly" && method !== "curl" ) {
440- await migrateToStandaloneForNightly ( method , target , versionArg ) ;
441- log . success ( `Successfully installed nightly ${ target } .` ) ;
442- return ;
507+ const warnings = await migrateToStandaloneForNightly (
508+ method ,
509+ target ,
510+ versionArg
511+ ) ;
512+ return {
513+ data : {
514+ action : downgrade ? "downgraded" : "upgraded" ,
515+ currentVersion : CLI_VERSION ,
516+ targetVersion : target ,
517+ channel,
518+ method,
519+ forced : flags . force ,
520+ warnings,
521+ } satisfies UpgradeResult ,
522+ } ;
443523 }
444524
445525 await executeStandardUpgrade ( {
@@ -450,8 +530,15 @@ export const upgradeCommand = buildCommand({
450530 execPath : this . process . execPath ,
451531 } ) ;
452532
453- log . success (
454- `Successfully ${ downgrade ? "downgraded" : "upgraded" } to ${ target } .`
455- ) ;
533+ return {
534+ data : {
535+ action : downgrade ? "downgraded" : "upgraded" ,
536+ currentVersion : CLI_VERSION ,
537+ targetVersion : target ,
538+ channel,
539+ method,
540+ forced : flags . force ,
541+ } satisfies UpgradeResult ,
542+ } ;
456543 } ,
457544} ) ;
0 commit comments