Dashboard › cli › Distillation
5050c884-f6a0-44e9-8f09-36306630558b["lore_tm_v1_DIgJeoYfCyJ_d75IkTNQu2yN-inYOC0TT4P9Hh07_ls","lore_tm_v1_CZF7gXZp56lPc1F0y0R59nr2WgoqGeHH5bjxh9PNL-A"]
packages/cli/src/lib/delta-upgrade.ts is a 627-line delta-upgrade implementation backed by binpatch; it preserves the existing public API through re-exports of ExtractStableChainOpts, GitHubAsset, GitHubRelease, PatchChain, StableChainInfo, extractSha256, getPatchFromVersion, getPatchTargetSha256, getStableTargetSha256, and PATCH_TAG_PREFIX.DeltaResult in packages/cli/src/lib/delta-upgrade.ts contains exact fields sha256: string, patchBytes: number, and chainLength: number.packages/cli/src/lib/delta-upgrade.ts imports GHCR_REPO from ./ghcr.js as the single source of truth for ghcr.io/getsentry/cli, avoiding a silent 404 previously caused by a string literal.logger.withTag("delta-upgrade"); HTTP instrumentation uses withTracing(name, "http.client", fn).patch-chain:${fromVersion}-${toVersion}.instrumentCache(base) wraps PatchCache.load() in a cache.get span and records "cache.key", "cache.hit", and, on a hit, "cache.item_size" from result.totalSize.instrumentCache(base) wraps nonempty PatchCache.save() operations in a cache.put span keyed from the first step’s fromVersion and last step’s toVersion; "cache.item_size" is the sum of chain.patches[].size. Empty-step saves delegate directly to base.save(chain, steps), while cleanup() and clear() directly delegate.getPatchCache() creates an instrumented binpatch cache at join(getConfigDir(), "patch-cache").stableSource() creates a githubReleaseSource using GITHUB_RELEASES_URL, getPlatformBinaryName(), user agent sentry-cli/${CLI_VERSION}, customFetch, and the HTTP instrument hook.nightlySource() creates a ghcrSource using registry "https://ghcr.io", repo GHCR_REPO, getPlatformBinaryName(), target tags of the form nightly-${version}, compareVersions, user agent sentry-cli/${CLI_VERSION}, customFetch, and the HTTP instrument hook.canAttemptDelta(targetVersion) rejects delta upgrades when CLI_VERSION === "0.0.0-dev", when current and target versions belong to different nightly/stable channels, or when the target is a downgrade; otherwise it permits an attempt.fetchRecentReleases(signal?) requests ${GITHUB_RELEASES_URL}?per_page=12 with Accept: application/vnd.github.v3+json, user agent sentry-cli/${CLI_VERSION}, and the optional abort signal.fetchRecentReleases() returns [] for a non-OK response, a non-array JSON payload, or any thrown error; non-array responses log "GitHub releases response is not an array" with typeof data, and caught failures log "Failed to fetch recent releases from GitHub".downloadStablePatch(url, signal?) fetches with user agent sentry-cli/${CLI_VERSION} and returns a Uint8Array of the full response body only for an OK response; non-OK responses return null, and caught errors log "Failed to download stable patch" and return null.extractStableChain(opts) adapts binpatchExtractStableChain(opts) by returning null when the binpatch result contains "failure", otherwise returning the StableChainInfo.filterAndSortChainTags(allTags, currentVersion, targetVersion) delegates to binpatchFilterAndSortChainTags() with the local compareVersions comparator.ChainStepResult preserves three rich failure categories: "version-mismatch" with expected and nullable actual; "missing-layer" with layerName; and "size-exceeded" with layerSize and budget.validateChainStep(manifest, opts) first compares getPatchFromVersion(manifest) to opts.expectedFrom; a mismatch returns the detailed "version-mismatch" failure before invoking binpatch validation.binpatchValidateChainStep() rejects a step, local validateChainStep() searches for the OCI layer whose "org.opencontainers.image.title" equals opts.patchLayerName; finding it produces "size-exceeded" using the layer’s size and opts.sizeLimit, while not finding it produces "missing-layer".resolveStableChain(currentVersion, targetVersion, signal?) delegates to stableSource().resolveChain(currentVersion, targetVersion, signal).resolveNightlyChain(opts) accepts token, currentVersion, targetVersion, fullGzSize, optional preloadedTags, and optional signal; it constructs an OciClient for "https://ghcr.io" and GHCR_REPO using user agent sentry-cli/${CLI_VERSION} and customFetch.resolveNightlyChain() uses opts.preloadedTags when supplied; otherwise it calls client.listTags(opts.token, PATCH_TAG_PREFIX, opts.signal), filters/sorts those tags for the requested version range, and returns null when the chain has zero tags or exceeds MAX_NIGHTLY_CHAIN_DEPTH.resolveNightlyChain() fetches all selected manifests concurrently with Promise.all; its manifest-fetch catch silently returns null and is marked as a grandfathered silent catch under #1531, with proposed cleanup choices of log.debug(), log.warn(), or rethrowing.validateChainStep() rather than binpatch’s validator so telemetry retains "version-mismatch", "missing-layer", and "size-exceeded" instead of binpatch’s coarser "malformed" and "over_budget" reasons.resolveNightlyChain() validates continuity from previousVersion, expects a patch layer named ${getPlatformBinaryName()}.patch, and sets the remaining size limit to opts.fullGzSize * SIZE_THRESHOLD_RATIO - totalSize; validation failure writes "telemetry_reason" to the active Sentry span and returns null.tag.slice(PATCH_TAG_PREFIX.length), accumulates the layer digest and size, and records { fromVersion: previousVersion, toVersion }; the final manifest supplies expectedSha256 through getPatchTargetSha256(manifest, binaryName).previousVersion differs from opts.targetVersion, or which lacks a final expected SHA-256, sets "telemetry_reason" to "version-mismatch" and returns null.resolveNightlyChain() downloads every selected blob concurrently through client.downloadBlobBuffer(opts.token, digest, opts.signal), converts each to Uint8Array, and returns a PatchChain containing patches, the sum of downloaded byte lengths as totalSize, expectedSha256, and steps.applyPatchChain(chain, oldBinaryPath, destPath, onBytes?) runs in span "apply-patches" with operation "upgrade.delta.apply", records "patches.count" and "patches.total_bytes", and calls applyPatchChainInMemory() with the patch byte arrays.applyPatchChain() verifies the resulting digest exactly against chain.expectedSha256; mismatch throws Error("SHA-256 mismatch after patching: got ${sha256}, expected ${chain.expectedSha256}").makeProgressHandler(setMessage?) creates a fresh progress bar whenever the progress phase changes and tracks byte deltas via event.written - previousWritten, preventing cumulative byte values from being counted repeatedly."pct" because multi-hop chains total each hop’s newSize and can misleadingly show 930 MB for a 310 MB installation; "download"/"read" and other pre-apply phases retain "bytes" format because their totals are honest sizes."Applying patch(es)" for phase "apply" and "Processing patch(es)" otherwise; "done" events call progress?.done().telemetry() exposes an internal _source.current capture so attemptDeltaUpgrade() can retain source attribution even when patch application fails after successful chain resolution.telemetry().onResolved() stores the source, sets active-span attribute "delta.source", and debug-logs Resolved patch chain from ${source}: ${chain.patches.length} patch(es), ${formatBytes(chain.totalSize)} total.telemetry().onOfflineMiss() stores and records "delta.source" = "offline_miss"; onUnavailable(reason) records the given DeltaUnavailableReason as "telemetry_reason".resolveDelta(source, targetVersion, oldBinaryPath, destPath, offline?, setMessage?) calls binpatch resolveAndApply() with currentVersion: CLI_VERSION, the selected source, old/destination paths, getPatchCache(), offline state, makeProgressHandler(setMessage), and telemetry; it resolves to both the DeltaResult | null and captured source.resolveStableDelta() and resolveNightlyDelta() preserve their existing five-parameter public APIs, call resolveDelta() with stableSource() and nightlySource() respectively, and expose only the returned result rather than the internal source.attemptDeltaUpgrade(targetVersion, oldBinaryPath, destPath, offline?, setMessage?) immediately resolves to null when canAttemptDelta(targetVersion) is false; otherwise it chooses channel "nightly" or "stable" from the target version."upgrade.delta" with operation "upgrade.delta", initial attribute "delta.channel", and attributes "delta.from_version" and "delta.to_version".attemptDeltaUpgrade() records "delta.patch_bytes" and "delta.chain_length" and emits Sentry distribution metrics "upgrade.delta.patch_bytes" and "upgrade.delta.chain_length", each tagged with the selected channel."delta.result" = "unavailable"; both successful application and ordinary unavailability set span status { code: 1 }."delta.from_version", "delta.to_version", and "delta.channel"; the failure then falls back by returning null rather than aborting the full upgrade.attemptDeltaUpgrade() restores the captured chain source to active-span attribute "delta.source", preventing error spans from losing network/cache/offline_miss attribution.Delta upgrade failed (${message}), falling back to full download, set span status { code: 2 }, set "delta.result" = "error" and "delta.error" = message, and return null.prefetch(source, targetVersion, signal?) exits if delta is ineligible or the signal is already aborted; otherwise it resolves a chain from CLI_VERSION, exits if there are no chain.steps or cancellation occurred, and saves the chain and steps to getPatchCache().prefetchNightlyPatches(targetVersion, signal?) and prefetchStablePatches(targetVersion, signal?) call the shared prefetch() with nightlySource() and stableSource() respectively.packages/cli/src/lib/release-notes.ts, commit parsing skips messages containing "#skip-changelog", empty first lines, messages not matching CONVENTIONAL_COMMIT_RE, and conventional-commit prefixes absent from COMMIT_PREFIX_TO_CATEGORY.CATEGORY_ORDER; each section’s Markdown consists of - ${description} lines followed by a trailing newline.CHANGELOG_MAX_RELEASES = 30 in packages/cli/src/lib/release-notes.ts; this is deliberately higher than delta-upgrade’s cap of 12, while remaining below GitHub’s maximum per_page of 100, and is intended to cover approximately 6+ months of weekly releases.fetchReleasesForChangelog() requests ${GITHUB_RELEASES_URL}?per_page=${CHANGELOG_MAX_RELEASES} with getGitHubHeaders() and returns releases newest first.fetchReleasesForChangelog() returns [] on request failure, non-OK response, JSON parse failure, or a non-array payload; it logs "Failed to fetch releases for changelog", "Non-JSON response from GitHub releases", or "GitHub releases response is not an array" as applicable.fetchStableChangelog(fromVersion, toVersion, maxItems?, prefetchedReleases?) uses caller-provided releases when available to avoid a duplicate API call; otherwise it uses the higher-cap changelog fetch, returns null for no releases, and delegates to buildChangelogSummary(releases, fromVersion, toVersion, maxItems).buildNightlyChangelogSummary(commits, fromVersion, toVersion, maxItems?) parses GitHub commit messages into sections and delegates to buildSummaryFromSections().X.Y.Z-dev.<unix-seconds>, enabling fetchNightlyChangelog() to query the GitHub Commits API by timestamp without git tags.fetchNightlyChangelog() extracts timestamps from both current and target nightly versions; if either extraction returns null, it debug-logs "Cannot extract timestamps from nightly versions" and returns null.since bound is inclusive, fetchNightlyChangelog() adds 1s to the current version timestamp to exclude the commit the user already has; because GitHub’s until bound is exclusive, it adds 1s to the target timestamp to include the target commit.