Dashboard › cli › Distillation
50e4649c-24b5-4578-b65e-eeb94d9c8cad["lore_tm_v1_dYKf_YonPn0uiESqvqpfSAsi0xBxtrj1W7rnXEy37eE","lore_tm_v1_Nn4JATR6eYDCMAMk92edzUCYJzdddqYC_zTzS_Jm3PM"]
Date: Sep 9, 2026
packages/cli/src/commands/cli/upgrade.ts implements sentry cli upgrade: self-updates to the latest or a specific version, then spawns the new binary with cli setup to refresh completions and agent skills and record installation metadata."stable" and "nightly" release channels. Stable is the default and tracks the latest GitHub release; nightly tracks the rolling prerelease built from main. The channel can be selected with --channel or positional "nightly"/"stable".UpgradeResult fields are action: "upgraded" | "downgraded" | "up-to-date" | "checked", currentVersion, targetVersion, channel: "stable" | "nightly", method, forced, and optional offline, warnings, and changelog.UpgradeFlags contains check, force, offline, "no-agent-skills", optional method?: InstallationMethod, and wrapper-injected optional json.resolveChannelAndVersion(positional) matches positional "nightly" and "stable" case-insensitively as channel selectors and returns no versionArg; otherwise it uses getReleaseChannel() and treats the positional value as a specific version.resolveOfflineTarget(versionArg) strips a leading v from an explicit version. Without one, it reads latestVersion from getVersionCheckInfo() and throws UpgradeError("network_error", "No cached version available. Run any command to trigger a background version check, then retry.") if absent.resolveTargetWithFallback() rejects an offline channel switch without a specific version because cached version data belongs to the old channel; its error instructs the user to run sentry cli upgrade with network access. It reads cached version information before persisting a changed channel because setReleaseChannel() clears the version cache.offline: "explicit". Online resolution persists the channel first and calls resolveTargetVersion(). Automatic cached fallback is allowed only for curl installs and only for UpgradeError reason "network_error"; it returns offline: "network-fallback". If the cache also lacks a target, the original network error is rethrown.validateMethod() permits --offline only for "curl" and otherwise throws UpgradeError("unsupported_operation", "Offline upgrade is only supported for curl-installed binaries.").validateMethod() throws UpgradeError("unknown_method") for "unknown". Homebrew plus a pinned stable version is rejected because the formula controls versioning; the error directs users to brew upgrade getsentry/tools/sentry.resolveTargetVersion() always fetches the channelβs latest version first, strips a leading v from a supplied version, supports check-only results, and skips an already-current target unless --force is set or the channel changed.resolveTargetVersion() therefore calls versionExists("curl", target) for the nightly channel regardless of the current installation method; stable uses the detected method. A missing pinned version throws UpgradeError("version_not_found", \Version ${target} not found`)`.buildCheckResult() returns action "checked" and, when the target differs from CLI_VERSION, adds exactly one update warning: an explicit pinned target uses Run 'sentry cli upgrade ${target}' to update., while channel/latest checks use Run 'sentry cli upgrade' to update.spawnWithRetry() uses SPAWN_MAX_ATTEMPTS = 5 and SPAWN_RETRY_BASE_MS = 500; retry delay is attempt * 500 ms, producing delays of 500 ms, 1000 ms, 1500 ms, and 2000 ms before subsequent attempts.isEbusyError(error) recognizes an Error whose Node errno code is "EBUSY". spawnWithRetry() retries only these failures, intended for Windows Defender/SmartScreen locks on freshly written executables; non-EBUSY failures such as EACCES throw immediately, and the final EBUSY also throws.isEnoentSpawnError(error) accepts standard errno code "ENOENT" and Bunβs message containing Executable not found in $PATH. spawnWithRetry() translates either into UpgradeError("execution_failed", \Downloaded binary not found at ${binaryPath}. The download may have been interrupted β rerun `sentry cli upgrade`.`)`.spawnWithRetry() spawns with inherited stdio and the supplied environment. A child terminated by "SIGKILL" is translated to an execution_failed error explaining that the OS likely rejected an invalid code signature and recommending curl -sL https://sentry.io/get-cli/ | bash; a null close code maps to 1.runSetupOnNewBinary() invokes the new executable with ordered arguments cli setup --quiet --method <method> --channel <channel> --no-modify-path, conditionally adding --install, --ensure-auth-scopes, and --no-agent-skills. When installDir is provided, the child receives SENTRY_INSTALL_DIR=<installDir>. A nonzero result throws UpgradeError("execution_failed", \Setup failed with exit code ${exitCode}`)`.resolveUpdatedCliPath(execPath, entryPath, pathEnv) selects whichSync("sentry", { PATH: pathEnv }), then entryPath, then execPath; this ensures package-manager setup uses the newly installed CLI rather than Nodeβs executable or a removed Homebrew keg path.resolveUpgradeInstallDir(currentInstallDir, pathEnv) normally retains the current curl install directory. For legacy join(homedir(), LEGACY_INSTALL_SUBDIR) (~/.sentry/bin), it removes the SENTRY_INSTALL_DIR pin, computes the XDG target with determineInstallDir(), and relocates only when that distinct XDG directory is already on PATH; otherwise it preserves the legacy directory.executeStandardUpgrade() uses rolling NIGHTLY_TAG only for latest-nightly upgrades without a specific version; pinned versions use their own tag. It wraps executeUpgrade() in withProgress({ message: \Downloading ${target}...`, json })and logs deltapatchBytesthroughformatBytes`.executeStandardUpgrade() runs setup from tempBinaryPath with install: true, a pinned install directory, ensureAuthScopes: !json, and the requested agent-skills behavior; it releases downloadResult.lockPath in finally. For package-manager/Homebrew results, it resolves the updated PATH executable and runs setup with install: false.migrateToStandaloneForNightly() handles users switching from brew/npm/pnpm/bun/yarn because nightly builds are standalone-only: 1. download through executeUpgrade("curl", ...), 2. install into determineInstallDir(homedir(), process.env), 3. run setup with method "curl" and channel "nightly", 4. return warnings that the old package-manager executable may shadow the standalone binary.NIGHTLY_TAG, while a pinned nightly uses its specific release tag. A missing download result throws UpgradeError("execution_failed", "Failed to download nightly binary"); the download lock is always released after setup.npm uninstall -g sentry; pnpm β pnpm remove -g sentry; bun β bun remove -g sentry; yarn β yarn global remove sentry; brew β brew uninstall getsentry/tools/sentry. Returned warnings first say Your ${method}-installed sentry may still appear earlier in PATH., then Consider removing it: ${hint} when a hint exists.persistChannel() writes the channel when it changed or when the supplied positional value is exactly in CHANNEL_VERSIONS; it must run after offline cache resolution because channel changes clear cached version-check data.startChangelogFetch() starts fetchChangelog({ channel, fromVersion, toVersion }) in parallel with download, but returns undefined without fetching when offline or when current and target versions match. It never throws: failures are swallowed so upgrade is not blocked.buildCheckResultWithChangelog() marks result.offline = true for either offline mode and awaits the changelog promise.upgradeCommand uses auth: false, human formatter formatUpgradeResult, and the brief Update the Sentry CLI to the latest version.sentry cli upgrade β update to latest using the persisted channel; 2. sentry cli upgrade nightly β βSwitch to nightly channel and updateβ; 3. sentry cli upgrade stable β switch back to stable and update; 4. sentry cli upgrade 0.5.0 β install a specific stable version; 5. sentry cli upgrade --check β check without installing; 6. sentry cli upgrade --force β force re-download when current; 7. sentry cli upgrade --method npm β force npm; 8. sentry cli upgrade --offline β use cached patches without network; 9. sentry cli upgrade --no-agent-skills β skip reinstalling agent skills.check: false, force: false, offline: false, and "no-agent-skills": false. method accepts curl, brew, npm, pnpm, bun, or yarn."Checking for updates...". Online check-only and up-to-date results exit early; check-only with a version difference fetches changelog before returning.--check is explicitly guarded from performing an upgrade and returns a structured check result. An already-current offline target returns "up-to-date" with offline: true unless forced or switching channels.channel === "nightly" && method !== "curl"; all other combinations use executeStandardUpgrade(). The final result reports "downgraded" or "upgraded" using isDowngrade(CLI_VERSION, target) and includes target, channel, original method, force state, optional offline marker, warnings, and changelog.packages/cli/src/lib/version-check.ts implements non-blocking background update checks and cached update notifications. Nightly checks use GHCR OCI-manifest annotations; stable checks use GitHub Releases.CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000, NOTIFICATION_INTERVAL_MS = 24 * 60 * 60 * 1000, and JITTER_FACTOR = 0.2.shouldCheckForUpdate() checks immediately when lastChecked === null; otherwise it applies Β±20% jitter and probability 1 - Math.exp(-elapsed / effectiveInterval), yielding approximately 0% at 0 intervals, 63% at 1 interval, and 86% at 2 intervals."upgrade", "--version", "-V", "--json", "token", or "init". CLI management subcommands "setup" and "fix" are suppressed only when identified after the "cli" command group.skipGlobalValueFlagValue() uses names derived from value-kind GLOBAL_FLAGS; it skips a following spaced value only when the token is a recognized --<name> without =, a next token exists, and that token does not begin with -.cliGroupIndex() scans past leading global flags and their values, stops at --, and accepts "cli" only as the first non-flag command token. cliSubcommandAfterGroup() similarly skips interleaved global flags and values after "cli" and stops at --; this handles forms such as sentry --verbose cli setup, sentry cli --verbose setup, and --org acme.abortPendingVersionCheck() aborts pendingAbortController if present and resets it to null, allowing process exit.maybePrefetchPatches(channel, latestVersion, signal) runs only when semverCompare(latestVersion, CLI_VERSION) === 1; nightly calls prefetchNightlyPatches(), stable calls prefetchStablePatches(). Patch-prefetch and subsequent cleanupPatchCache() failures are independently caught and debug-logged as best-effort.checkForUpdateInBackgroundImpl() catches DB failures from scheduling logic with Sentry.captureException(error) and returns without crashing the CLI.checkForUpdateInBackgroundImpl() starts a detached manual Sentry span with name: "version-check", op: "version.check", and forceTransaction: true; nightly calls fetchLatestNightlyVersion(signal), stable calls fetchLatestFromGitHub(signal), then it persists setVersionCheckInfo(latestVersion) and prefetches delta patches."version_check.error" to the message and "version_check.error_type" to the constructor name rather than creating Sentry issues; success uses span status code 1, failure status code 2, and finally clears the controller and ends the span.process.stderr.isTTY; non-TTY cases include scripts, CI logs, pipes, and editor-captured output. The behavior is documented as matching the gh CLI.notifiedThisProcess prevents duplicate banners within one process. Cross-invocation rate limiting uses DB-backed lastNotified, with first notification allowed when null and later notifications allowed only after at least NOTIFICATION_INTERVAL_MS.getUpdateNotificationWithCopy() returns null for non-TTY stderr, duplicate same-process calls, absent cached version, a non-newer version, daily rate limiting, or DB errors.getUpdateNotificationWithCopy() calls markUpdateNotified() before returning and sets notifiedThisProcess = true. Persistence failure is captured with Sentry.captureException(error) but does not suppress the banner.formatStandardUpdateNotification() labels nightly updates "New nightly available:" and stable updates "Update available:", displays ${CLI_VERSION} -> ${latestVersion}, and instructs users to run "sentry cli upgrade".formatContextualUpdateNotification() says: A new version of sentry-cli is available (${latestVersion}). Upgrading may resolve this β we fix a lot of bugs in every release. Run "sentry cli upgrade" to update.resetUpdateNotificationState() resets notifiedThisProcess = false for tests and is marked internal, not for production use.SENTRY_CLI_NO_UPDATE_CHECK="1" disables both background checks and notification retrieval, supporting test isolation.maybeCheckForUpdateInBackground() is fire-and-forget and calls checkForUpdateInBackgroundImpl() unless update checks are disabled.getErrorUpdateNotification(error, args) first honors command suppression. User errors identified by isUserError(error) receive the standard neutral banner; non-user errors receive the contextual βupgrading may resolve thisβ copy. Both paths share TTY, once-per-process, and daily-rate-limit gates, preventing double emission.