Skip to content

Commit 81f11d0

Browse files
authored
feat(cli): add toolkit upgrade bridge (#1569)
## Summary - check Toolkit before the legacy CLI repositories for stable, nightly, delta, prefetch, and changelog requests - fall back only on HTTP 404 and retain one selected source through the complete upgrade operation - mirror the ordered source contract in the shell installer and enforce parity in CI Closes #1244 ## Validation - `pnpm run lint` - `pnpm run typecheck` - `pnpm run check:deps` - `pnpm run check:errors` - `pnpm run check:patches` - `pnpm run check:stale-refs` - `pnpm run check:upgrade-sources` - `shellcheck packages/cli/install` - focused bridge matrix: 458 tests passed - isolated rerun of the 10 full-suite failure files: 252 tests passed; the full coverage run had unrelated temp-directory/SQLite contention and one load timeout
1 parent b1749f0 commit 81f11d0

17 files changed

Lines changed: 3299 additions & 433 deletions

‎src/commands/cli/upgrade.ts‎

Lines changed: 122 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,11 @@ import type { SentryContext } from "../../context.js";
2222
import {
2323
determineInstallDir,
2424
isDowngrade,
25+
isNightlyVersion,
2526
LEGACY_INSTALL_SUBDIR,
2627
releaseLock,
2728
samePath,
29+
type UpgradeSource,
2830
} from "../../lib/binary.js";
2931
import { buildCommand } from "../../lib/command.js";
3032
import { CLI_VERSION } from "../../lib/constants.js";
@@ -34,7 +36,7 @@ import {
3436
setReleaseChannel,
3537
} from "../../lib/db/release-channel.js";
3638
import { getVersionCheckInfo } from "../../lib/db/version-check.js";
37-
import { UpgradeError } from "../../lib/errors.js";
39+
import { UpgradeError, UpgradeTransportError } from "../../lib/errors.js";
3840
import { formatUpgradeResult } from "../../lib/formatters/human.js";
3941
import { formatBytes } from "../../lib/formatters/numbers.js";
4042
import { 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: {
233244
function 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+
260277
type ResolveTargetOptions = {
261278
method: InstallationMethod;
262279
channel: ReleaseChannel;
@@ -273,8 +290,28 @@ type ResolveTargetOptions = {
273290
* (check-only mode, or already up to date)
274291
*/
275292
type 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

Comments
 (0)