Dashboard › cli › Distillation
b7467011-51bd-400b-a569-e1dd93a6f307["lore_tm_v1_cPAgNZsApLEVoC5HUxCh2DNbzINKTtkaQ8OcHsOW0Q0","lore_tm_v1_kfNIfmwOQ0BSzDSdK2BeWCFNztAabC3z8F008LBs0Qw","lore_tm_v1_3UsCZYDqwufjyMRzGLGRZ6AyuxtDUjOpnpnBa3WS8-U","lore_tm_v1_PE-O8LUsVT4ON89nBWEDbXo6AtsS54eQFqWdOwUeRnM"]
6e3e7e13a feat(config): follow XDG Base Directory spec for config location (#1503); 2. b0b22fe82 feat(errors): wire up no-silent-catch lint rule, drop ratchet baseline (#1532); 3. d9af07158 fix(version-check): log errors in maybePrefetchPatches catch blocks (#1512); 4. 1837850d6 feat(upgrade): add --no-agent-skills flag (#1407); 5. 5a584348b fix(auth): request team admin OAuth scope (#1373); 6. 2c54e1a1f ref(cli): replace argv-hoist preprocessor with a Stricli top-level-flags patch (#1340); 7. 7b429edd1 fix(upgrade): use generated-patch from-version + close output fd synchronously (#1327); 8. 96390e5f6 chore: pre-shape repo into pnpm-workspace monorepo layout (#1254).2c54e1a1f replaces the argv-hoist preprocessor with a Stricli top-level-flags patch in #1340.detectInstallationMethod() in packages/cli/src/lib/upgrade.ts follows the requirement: βAlways check for Homebrew first β the stored install info may be staleβ; legacy detection remains available for existing pre-setup-command installs, and failure to persist install information is debug-logged as "Failed to persist install info (DB may be read-only)".downloadNightlyToPath(destPath, version?, setMessage?) in packages/cli/src/lib/upgrade.ts fetches a pinned manifest as nightly-${version} when version is supplied, otherwise calls fetchNightlyManifest(token) for the rolling :nightly tag; it locates getNightlyGzFilename(), downloads the selected OCI layer by digest, and streams decompression to destPath.downloadNightlyToPath() throws UpgradeError("execution_failed", "GHCR blob response had no body") when the nightly blob response has no body.downloadStableToPath(version, destPath, setMessage?) first tries ${getBinaryDownloadUrl(version)}.gz, described as approximately 37 MB versus approximately 99 MB raw and approximately 60% smaller, and streams it through DecompressionStream; any compressed-path failure falls through to the raw binary URL.downloadStableToPath() is marked biome-ignore lint/plugin as a grandfathered silent catch tracked by #1531, with the stated cleanup options of adding log.debug()/log.warn() or rethrowing.downloadStableToPath() throws UpgradeError("execution_failed", \Failed to download binary: HTTP ${response.status}`)`.response.arrayBuffer() and then call writeFile(destPath, new Uint8Array(body)); this avoids a Bun event-loop bug where Bun.write(path, Response) can exit before a large streaming download finishes, documented at https://github.com/oven-sh/bun/issues/13237.packages/cli/src/lib/upgrade.ts uses VERIFY_MAX_ATTEMPTS = 6 and VERIFY_BASE_DELAY_MS = 100; probeBinaryFile(path) returns the size only when statSync(path, { throwIfNoEntry: false }) finds a regular file with stats.size > 0, otherwise it returns null.waitForBinaryVisible(path) addresses Windows + Bun 1.3.9 issue CLI-1D3, where Bun.file().writer().end() can return before the OS exposes the written file and a subsequent Bun.spawn fails with Executable not found in $PATH.waitForBinaryVisible() uses this exact exponential-backoff schedule: attempt 1 probes at 0 ms, then sleeps 100 ms; attempt 2 probes at 100 ms, then sleeps 200 ms; attempt 3 probes at 300 ms, then sleeps 400 ms; attempt 4 probes at 700 ms, then sleeps 800 ms; attempt 5 probes at 1500 ms, then sleeps 1600 ms; attempt 6 probes at 3100 ms with no further sleep. Total worst-case wall-clock budget is approximately 3.1s.waitForBinaryVisible() logs "Binary became visible after ${attempt} attempts" when recovery takes more than one probe and logs each retry as Downloaded binary not yet visible at ${path}, retrying in ${delay}ms (attempt ${attempt}/${VERIFY_MAX_ATTEMPTS}).waitForBinaryVisible() throws when the file βnever becomes visible or stays empty,β using UpgradeError("execution_failed", \Downloaded binary is missing or empty at ${path}. This is usually transient β rerun `sentry cli upgrade` to retry.`)`.downloadBinaryToTemp(version, downloadTag?, offline?, setMessage?) acquires the lock from getCurlInstallPaths(), holds it across the downloadβspawnβinstall pipeline, and requires the caller to release it after the child exits; if the child resolves to the same install path, acquireLock recognizes process.ppid, takes over the lock, and makes the parentβs later release a harmless no-op.downloadBinaryToTemp() best-effort deletes an interrupted-download temp file with unlinkSync(tempPath); its ignored missing-file catch is another #1531 grandfathered silent catch.downloadBinaryToTemp() tries tryDeltaUpgrade(version, tempPath, !!offline, setMessage) before a full binary download; delta failure, including missing patches or hash mismatch, falls back to the full download when online, and a successful delta records deltaResult.patchBytes.UpgradeError("offline_cache_miss", \Cannot upgrade to ${version} in offline mode β no pre-downloaded update is available. Run `sentry cli upgrade` without `--offline` to download the update directly.`)`.UpgradeError("offline_cache_miss", \Cannot upgrade to ${version} β the network is unavailable and no pre-downloaded update was found. Check your internet connection and try again.`)`."Downloading full binary" and calls downloadFullBinary(version, downloadTag, tempPath, setMessage).downloadBinaryToTemp() calls waitForBinaryVisible(tempPath) before spawning, then logs Binary verified (${formatBytes(verifiedSize)}); this turns the Windows/Bun filesystem-visibility race into a self-healing retry rather than the opaque Executable not found in $PATH: "...sentry.exe.download" failure.downloadBinaryToTemp() asynchronously calls clearPatchCache() because patches for the old binary are useless after either a delta or full upgrade; cache-cleanup failure is best-effort and does not fail the upgrade.downloadBinaryToTemp() applies chmodSync(tempPath, 0o755) on platforms other than win32, returns { tempBinaryPath: tempPath, lockPath, patchBytes }, and releases the lock before rethrowing any error.tryDeltaUpgrade(version, destPath, offline?, setMessage?) delegates to attemptDeltaUpgrade(version, process.execPath, destPath, offline, setMessage) and returns DeltaResult | null.downloadFullBinary() chooses GHCR nightly download when isNightlyVersion(version) is true by calling downloadNightlyToPath(destPath, version, setMessage); otherwise it downloads stable from GitHub Releases with downloadStableToPath(downloadTag ?? version, destPath, setMessage).executeUpgradeHomebrew() spawns brew upgrade getsentry/tools/sentry with stdio: "inherit" and shell: process.platform === "win32"; Homebrew intentionally ignores the target version because arbitrary release pinning is unsupported.UpgradeError("execution_failed", \brew upgrade failed with exit code ${code}`)and spawn errors withUpgradeError("execution_failed", `brew failed: ${err.message}`)`.executeUpgradePackageManager(pm, version) uses ["global", "add", \sentry@${version}`]for yarn and["install", "-g", `sentry@${version}`]for npm, pnpm, and bun; it enablesshell: trueon Windows because npm/pnpm/yarn executables are.cmdfiles that otherwise produceENOENT`.UpgradeError("execution_failed", \${pm} install failed with exit code ${code}`)and spawn errors withUpgradeError("execution_failed", `${pm} failed: ${err.message}`)`.executeUpgrade(method, version, downloadTag?, offline?, setMessage?) dispatches "curl" to downloadBinaryToTemp(), "brew" to executeUpgradeHomebrew(), and "npm", "pnpm", "bun", or "yarn" to executeUpgradePackageManager(); package-manager paths return null, and the default throws UpgradeError("unknown_method").packages/cli/src/lib/version-check.ts checks nightly builds, identified when CLI_VERSION contains "-dev.<timestamp>", against GHCR OCI manifest annotations; stable builds check GitHub Releases. Results are cached in the database for later runs.CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000, notification banners are limited by NOTIFICATION_INTERVAL_MS = 24 * 60 * 60 * 1000, and probabilistic check timing uses JITTER_FACTOR = 0.2 for Β±20% jitter.1 - Math.exp(-elapsed / effectiveInterval), where effectiveInterval = CHECK_INTERVAL_MS * (1 + jitter) and jitter = (Math.random() - 0.5) * 2 * JITTER_FACTOR; documented probabilities are approximately 0% at 0% of the interval, 63% at 100%, and 86% at 200%."upgrade", "--version", "-V", "--json", "token", or "init"; "init" is explicitly suppressed because its interactive wizard has its own terminal UI."setup" and "fix" in SUPPRESSED_CLI_SUBCOMMANDS, only when found as a subcommand of the "cli" group to avoid false positives such as --project setup.GLOBAL_VALUE_FLAG_NAMES is derived from GLOBAL_FLAGS.filter((f) => f.kind === "value").map((f) => f.name).skipGlobalValueFlagValue(args, i) advances over a spaced value only when the token starts with --, its stripped name belongs to GLOBAL_VALUE_FLAG_NAMES, the token does not contain "=", the next token exists, and the next token does not start with "-".cliGroupIndex(args) supports global flags before cli, stops at "--", skips spaced values belonging to global value flags, and accepts cli only when it is the first non-flag command token.cliSubcommandAfterGroup(args, start) supports global flags between cli and its subcommand, skips their spaced values, and stops at a "--" escape.sentry --verbose cli setup, sentry cli --verbose setup, and sentry cli --org acme setup must still identify the correct command group and subcommand.shouldSuppressNotification(args) first checks SUPPRESSED_ARGS, then locates the cli group and its real subcommand past interleaved global flags, returning true for cli setup and cli fix.pendingAbortController: AbortController | null; abortPendingVersionCheck() aborts the controller and resets it to null to allow process exit.maybePrefetchPatches(channel, latestVersion, signal) does nothing unless semverCompare(latestVersion, CLI_VERSION) === 1; it calls prefetchNightlyPatches() for "nightly" and prefetchStablePatches() for "stable".d9af07158 changed the best-effort catches in maybePrefetchPatches() to log errors: prefetch failures use logger.debug("Delta patch pre-fetch failed (best-effort)", error), and stale cache cleanup failures use logger.debug("Patch cache cleanup failed (best-effort)", error).maybePrefetchPatches() opportunistically calls cleanupPatchCache().checkForUpdateInBackgroundImpl() does not block and βNever throws - errors are caught and reported to Sentry.βshouldCheckForUpdate() itself fails, checkForUpdateInBackgroundImpl() reports the database-access error with Sentry.captureException(error) and returns without crashing the CLI.Sentry.startSpanManual() with { name: "version-check", op: "version.check", forceTransaction: true }; nightly uses fetchLatestNightlyVersion(signal), stable uses fetchLatestFromGitHub(signal), then setVersionCheckInfo(latestVersion) and maybePrefetchPatches(channel, latestVersion, signal) run.span.setStatus({ code: 1 }); failures use span.setStatus({ code: 2 }); pendingAbortController is cleared and span.end() is called in finally.AbortError failures are not reported. Other background network and JSON failures are stored as span attributes "version_check.error" and "version_check.error_type" rather than sent through captureException, because transient GitHub rate limits and CDN errors should remain queryable in Discover without cluttering the Issues feed.process.stderr.isTTY; non-TTY stderr includes scripts, CI logs, pipes, and editor captures, and suppression matches gh CLI behavior.notifiedThisProcess prevents a single process from returning the update notification more than once, including flows such as sentry help piped into less.canNotifyAgain(lastNotified) returns true for null; otherwise it requires Date.now() - lastNotified >= NOTIFICATION_INTERVAL_MS, with the DB-backed last_notified timestamp carrying the 24-hour rate limit across invocations.getUpdateNotificationWithCopy(formatNotification) returns null when current, lacking cached version data, rate-limited, running with non-TTY stderr, or encountering an error; it βNever throws β errors are caught and reported to Sentry.βgetUpdateNotificationWithCopy() returns a non-null message, it persists last_notified = now through markUpdateNotified() so later invocations within the rate-limit window return null.