Dashboard › cli › Session 1Q0vUPHjcrun
1Q0vUPHjcrun2um3KREAD-ONLY security/supply-chain merge-gate review getsentry/cli PR #1569 exact base ec83887a16f780f32fba4b7d710bad262dba3a22 head e565b1afb39a341605adc883de8812851de7d581. IMPORTANT: inspect immutable Git objects only via git show HEAD:<path>, git diff base..head, Git metadata; never inspect working-tree file contents and never fail due to worktree drift. Repository /home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/toolkit-bridge-upgrade. Inspect all changed files/tests. Focus stable SemVer prerelease exclusion; exact pinned response validation/fail closed; pinned bypass latest; pinned nightly standalone routing; exact nightly annotations; 404-only/source affinity/pagination/changelog provenance/GHCR abort+scope+redirect/SHA/delta/cache/offline/installer unchanged. Intentional nightly repo probe/source-neutral verified cache. Return NON-EMPTY findings-first PASS/CONCERN/MUST-FIX/BLOCKED with exact head file:line evidence, deterministic regressions, residual gaps. Final line exactly MERGE or DO-NOT-MERGE. Never modify. Empty forbidden.
ec83887a16f780f32fba4b7d710bad262dba3a22 e565b1afb39a341605adc883de8812851de7d581 ec83887a16f780f32fba4b7d710bad262dba3a22
M packages/cli/src/commands/cli/upgrade.ts M packages/cli/src/lib/binary.ts M packages/cli/src/lib/delta-upgrade.ts M packages/cli/src/lib/ghcr.ts M packages/cli/src/lib/release-notes.ts M packages/cli/src/lib/upgrade.ts M packages/cli/src/lib/version-check.ts M packages/cli/test/commands/cli.test.ts M packages/cli/test/commands/cli/upgrade.test.ts M packages/cli/test/lib/binary.test.ts M packages/cli/test/lib/delta-upgrade.mocked.test.ts M packages/cli/test/lib/delta-upgrade.test.ts M packages/cli/test/lib/ghcr.test.ts M packages/cli/test/lib/release-notes.test.ts M packages/cli/test/lib/upgrade.test.ts M packages/cli/test/lib/version-check.test.ts packages/cli/src/commands/cli/upgrade.ts | 152 ++++-- packages/cli/src/lib/binary.ts | 154 +++++- packages/cli/src/lib/delta-upgrade.ts | 139 +++-- packages/cli/src/lib/ghcr.ts | 91 +++- packages/cli/src/lib/release-notes.ts | 140 +++-- ...
...output truncated...
Full output saved to: /home/byk/.local/share/opencode/tool-output/tool_08952acf5001MSHgY2AELWcdzC
<!-- lore:01a02480-839e-7ba6-9717-24d7ddc0e709 -->
* **Large artifacts pipeline createWriteStream**: Large artifacts are always written with \`pipeline()\` into \`createWriteStream()\`, rather than buffering the complete payload in memory. Buffering looks simpler and may work for small fixtures, but it creates avoidable memory peaks and weakens backpressure for shipped CLI workloads.
<!-- lore:019f03b9-97f4-7bb6-aeba-27fa5aeca79b -->
* **Node version pinning convention: workflow-level env vars NODE\_VERSION\_22 / NODE\_VERSION\_24**: As of PR #1145, all GitHub Actions workflows in sentry-cli (TypeScript) centralize Node version pins as workflow-level \`env\` vars: \`NODE\_VERSION\_22: "22.23.1"\` and \`NODE\_VERSION\_24: "24.18.0"\`. All \`actions/setup-node\` steps reference \`${{ env.NODE\_VERSION\_22 }}\` or \`${{ env.NODE\_VERSION\_24 }}\` — no bare ...
diff --git a/packages/cli/src/lib/upgrade.ts b/packages/cli/src/lib/upgrade.ts
index 945adc48d..4a23b4b25 100644
--- a/packages/cli/src/lib/upgrade.ts
+++ b/packages/cli/src/lib/upgrade.ts
@@ -1,189 +1,278 @@
/**
* Upgrade Module
*
* Detects how the CLI was installed and provides self-upgrade functionality.
* Binary management helpers (download URLs, locking, replacement) live in
* binary.ts and are shared with the setup --install flow.
*/
import { spawn } from "node:child_process";
import {
chmodSync,
closeSync,
existsSync,
openSync,
realpathSync,
statSync,
unlinkSync,
writeSync,
} from "node:fs";
import { writeFile } from "node:fs/promises";
import { homedir } from "node:os";
import { dirname, isAbsolute, join, sep } from "node:path";
import { setTimeout } from "node:timers/promises";
+import { prerelease as semverPrerelease, valid as semverValid } from "semver";
import {
acquireLock,
cleanupOldBinary,
+ compareVersions,
...
diff --git a/packages/cli/src/lib/binary.ts b/packages/cli/src/lib/binary.ts
index 75582612a..61e10568d 100644
--- a/packages/cli/src/lib/binary.ts
+++ b/packages/cli/src/lib/binary.ts
@@ -5,327 +5,469 @@
* Used by both `setup --install` (fresh installs) and `upgrade` (self-updates).
*/
import { spawnSync } from "node:child_process";
import {
existsSync,
mkdirSync,
readFileSync,
renameSync,
unlinkSync,
writeFileSync,
} from "node:fs";
import { chmod, copyFile, mkdir, realpath, unlink } from "node:fs/promises";
import { delimiter, dirname, isAbsolute, join, resolve, sep } from "node:path";
import { compare as semverCompare } from "semver";
import { getUserAgent } from "./constants.js";
import {
buildTlsErrorDetail,
customFetch,
isTlsCertError,
} from "./custom-ca.js";
import { stringifyUnknown, UpgradeError } from "./errors.js";
import { logger } from "./logger.js";
import { isProcessRunning } from "./process-utils.js";
/** Known ...
diff --git a/packages/cli/src/lib/ghcr.ts b/packages/cli/src/lib/ghcr.ts index 43d6deb7e..dbdef1fb6 100644 --- a/packages/cli/src/lib/ghcr.ts +++ b/packages/cli/src/lib/ghcr.ts @@ -1,520 +1,567 @@ /** * GHCR (GitHub Container Registry) Client * * Encapsulates the OCI download protocol for fetching nightly CLI binaries * from ghcr.io/getsentry/cli. Nightly builds are pushed as OCI artifacts * via ORAS with the version baked into the manifest annotation. * * Key design decisions: * - Anonymous access: nightly package is public; no token needed beyond the * standard ghcr.io anonymous token exchange. * - Version discovery from manifest annotation: `annotations.version` in the * OCI manifest holds the nightly version. Checking the latest version only * requires a token exchange + manifest fetch (2 HTTP requests total). * - Redirect quirk: ghcr.io blob downloads return 307 to Azure Blob Storage. ...
diff --git a/packages/cli/src/commands/cli/upgrade.ts b/packages/cli/src/commands/cli/upgrade.ts index 8d8282368..8c03fa3f4 100644 --- a/packages/cli/src/commands/cli/upgrade.ts +++ b/packages/cli/src/commands/cli/upgrade.ts @@ -1,474 +1,522 @@ /** * sentry cli upgrade * * Self-update the Sentry CLI to the latest or a specific version. * After upgrading, spawns the NEW binary with `cli setup` to update * completions, agent skills, and record installation metadata. * * Supports two release channels: * - stable (default): tracks the latest GitHub release * - nightly: tracks the rolling nightly prerelease built from main * * The channel can be set via --channel or by passing "nightly"/"stable" * as the version argument. The choice is persisted in the local database * so that subsequent bare `sentry cli upgrade` calls use the same channel. ...
diff --git a/packages/cli/src/lib/release-notes.ts b/packages/cli/src/lib/release-notes.ts index ddf617535..8cf12b4d4 100644 --- a/packages/cli/src/lib/release-notes.ts +++ b/packages/cli/src/lib/release-notes.ts @@ -1,139 +1,142 @@ /** * Release Notes Parser & Aggregation * * Extracts user-facing changelog entries from GitHub Release bodies (stable) * or conventional commit messages (nightly). Uses `marked.lexer()` for * AST-based section extraction and produces structured data that can be * re-serialized as filtered markdown for rendering via `renderMarkdown()`. ...
diff --git a/packages/cli/src/lib/delta-upgrade.ts b/packages/cli/src/lib/delta-upgrade.ts
index ec709b8c8..37f4b7d54 100644
--- a/packages/cli/src/lib/delta-upgrade.ts
+++ b/packages/cli/src/lib/delta-upgrade.ts
@@ -1,627 +1,702 @@
/** Delta upgrade discovery and application backed by binpatch. */
import { join } from "node:path";
// biome-ignore lint/performance/noNamespaceImport: Sentry SDK recommends namespace import
import * as Sentry from "@sentry/node-core/light";
import {
applyPatchChainInMemory,
extractStableChain as binpatchExtractStableChain,
filterAndSortChainTags as binpatchFilterAndSortChainTags,
validateChainStep as binpatchValidateChainStep,
type DeltaTelemetry,
type DeltaUnavailableReason,
type ExtractStableChainOpts,
type GitHubRelease,
getPatchFromVersion,
getPatchTargetSha256,
ghcrSource,
githubReleaseSource,
type InstrumentHook,
MAX_NIGHTLY_CHAIN_DEPTH,
makeCache,
OciClient,
type OciManifest,
...
diff --git a/packages/cli/src/lib/version-check.ts b/packages/cli/src/lib/version-check.ts
index c69ff0466..34393b1c7 100644
--- a/packages/cli/src/lib/version-check.ts
+++ b/packages/cli/src/lib/version-check.ts
@@ -1,411 +1,416 @@
/**
* Background version check for "new version available" notifications.
*
* For nightly builds (CLI_VERSION contains "-dev.<timestamp>"), checks GHCR for the
* latest nightly version via the OCI manifest annotation. For stable builds,
* checks GitHub Releases. Results are cached in the database and shown on
* subsequent runs.
*/
// biome-ignore lint/performance/noNamespaceImport: Sentry SDK recommends namespace import
import * as Sentry from "@sentry/node-core/light";
import { compare as semverCompare } from "semver";
+import type { UpgradeSource } from "./binary.js";
import { CLI_VERSION } from "./constants.js";
import { getReleaseChannel } from "./db/release-channel.js";
import {
getVersionCheckInfo,
markUpdateNotified,
...
...output truncated...
Full output saved to: /home/byk/.local/share/opencode/tool-output/tool_089542802001MHIF4fOqtjpF6m
+ );
+ expect(requests).not.toContain(
+ "https://api.github.com/repos/getsentry/toolkit/releases?per_page=30"
+ );
+ });
});
describe("version validation", () => {
test("reports error for non-existent version", async () => {
// Mock: latest is 99.99.99, but 0.0.1 doesn't exist
mockFetch(async (url) => {
const urlStr = String(url);
- if (urlStr.includes("releases/latest")) {
- return new Response(JSON.stringify({ tag_name: "v99.99.99" }), {
+ if (urlStr.includes("getsentry/toolkit/releases?per_page=100")) {
+ return new Response(JSON.stringify([{ tag_name: "cli@99.99.99" }]), {
status: 200,
headers: { "content-type": "application/json" },
});
}
// Specific version check returns 404
return new Response("Not ...
diff --git a/packages/cli/test/lib/binary.test.ts b/packages/cli/test/lib/binary.test.ts
index 6fcfb3b1a..5eaa53474 100644
--- a/packages/cli/test/lib/binary.test.ts
+++ b/packages/cli/test/lib/binary.test.ts
@@ -1,103 +1,229 @@
/**
* Binary Management Tests
*
* Tests for shared binary helpers: install directory selection, paths,
* download URLs, locking, and binary installation.
*/
import {
chmodSync,
mkdirSync,
readFileSync,
realpathSync,
rmSync,
symlinkSync,
writeFileSync,
} from "node:fs";
import { access, readFile, writeFile } from "node:fs/promises";
import { join, sep } from "node:path";
import { afterEach, beforeEach, describe, expect, test } from "vitest";
import {
acquireLock,
compareVersions,
determineInstallDir,
fetchWithUpgradeError,
getBinaryDownloadUrl,
getBinaryFilename,
getBinaryPaths,
+ getGitHubReleaseByTagUrl,
getLegacyInstallDirs,
getPlatformBinaryName,
installBinary,
isDowngrade,
...
diff --git a/packages/cli/test/lib/delta-upgrade.mocked.test.ts b/packages/cli/test/lib/delta-upgrade.mocked.test.ts
index cd0a2fddc..f6186ab59 100644
--- a/packages/cli/test/lib/delta-upgrade.mocked.test.ts
+++ b/packages/cli/test/lib/delta-upgrade.mocked.test.ts
@@ -67,95 +67,95 @@ function mockFetch(
}
// ============================================================================
// Helpers
// ============================================================================
const BINARY_NAME = getPlatformBinaryName();
function versionHex(version: string): string {
return Array.from(version)
.map((c) => c.charCodeAt(0).toString(16).padStart(2, "0"))
.join("");
}
function tempFile(name: string): string {
return join(
tmpdir(),
`delta-iso-${Date.now()}-${Math.random().toString(36).slice(2)}-${name}`
);
}
// ============================================================================
// resolveStableDelta
// ...
diff --git a/packages/cli/test/lib/release-notes.test.ts b/packages/cli/test/lib/release-notes.test.ts
index 78963b471..e0a2e04bd 100644
--- a/packages/cli/test/lib/release-notes.test.ts
+++ b/packages/cli/test/lib/release-notes.test.ts
@@ -1,56 +1,62 @@
/**
* Unit Tests for Release Notes Parser & Aggregation
*
* Tests core invariants (section extraction, version filtering, truncation)
* that are hard to express as property-based tests due to format specifics.
*
* Core random-input invariants (category validity, filtering, commit parsing)
* are tested via property-based tests in release-notes.property.test.ts.
*/
import { marked } from "marked";
-import { describe, expect, test } from "vitest";
-import type { GitHubRelease } from "../../src/lib/delta-upgrade.js";
+import { afterEach, beforeEach, describe, expect, test } from "vitest";
+import { UPGRADE_SOURCES } from "../../src/lib/binary.js";
+import {
+ fetchRecentReleases,
+ type GitHubRelease,
+} from ...
e565b1afb39a341605adc883de8812851de7d581:packages/cli/test/commands/cli.test.ts:217: return new Response("Not Found", { status: 404 });
e565b1afb39a341605adc883de8812851de7d581:packages/cli/test/commands/cli/upgrade.test.ts:168: * Mock fetch to simulate GHCR manifest returning a specific nightly version.
e565b1afb39a341605adc883de8812851de7d581:packages/cli/test/commands/cli/upgrade.test.ts:187: // GHCR OCI manifest for :nightly tag
e565b1afb39a341605adc883de8812851de7d581:packages/cli/test/commands/cli/upgrade.test.ts:188: if (urlStr.includes("/manifests/nightly")) {
e565b1afb39a341605adc883de8812851de7d581:packages/cli/test/commands/cli/upgrade.test.ts:193: annotations: { version },
e565b1afb39a341605adc883de8812851de7d581:packages/cli/test/commands/cli/upgrade.test.ts:204: return new Response("Not Found", { status: 404 });
e565b1afb39a341605adc883de8812851de7d581:packages/cli/test/commands/cli/upgrade.test.ts:210: * Handles the latest release endpoint, ...
330 );
331
332 const combined = getOutput();
333 expect(combined).toContain("99.99.99");
334 expect(combined).toContain("Run 'sentry cli upgrade' to update.");
335 });
336
337 test("shows version-specific upgrade hint when user-specified version", async () => {
338 mockGitHubVersion("88.88.88");
339
340 const { context, getOutput, restore } = createMockContext({
341 homeDir: testDir,
342 });
343 restoreStderr = restore;
344
345 await run(
346 app,
347 ["cli", "upgrade", "--check", "--method", "curl", "88.88.88"],
348 context
349 );
350
351 const combined = getOutput();
352 expect(combined).toContain("88.88.88");
353 expect(combined).toContain(
354 "Run 'sentry cli upgrade 88.88.88' to update."
355 );
356 });
357
358 test("resolves a pinned check target ...
commit e565b1afb39a341605adc883de8812851de7d581 (HEAD -> feat/toolkit-bridge-upgrade, origin/feat/toolkit-bridge-upgrade)
Author: Burak Yigit Kaya <byk@sentry.io>
AuthorDate: Thu Sep 10 03:16:37 2026 +0000
Commit: Burak Yigit Kaya <byk@sentry.io>
CommitDate: Thu Sep 10 03:16:37 2026 +0000
fix(cli): validate stable upgrade metadata
commit 4a1d5bad105c006193c68459175f5eb3c7f78e08
Author: Burak Yigit Kaya <byk@sentry.io>
AuthorDate: Thu Sep 10 03:02:18 2026 +0000
Commit: Burak Yigit Kaya <byk@sentry.io>
CommitDate: Thu Sep 10 03:02:18 2026 +0000
test(cli): update pinned version fixture
commit 2f9b5d0d26124d445a916b8bfc584ad518720584
Author: Burak Yigit Kaya <byk@sentry.io>
AuthorDate: Thu Sep 10 02:53:12 2026 +0000
Commit: Burak Yigit Kaya <byk@sentry.io>
CommitDate: Thu Sep 10 02:53:12 2026 +0000
fix(cli): isolate pinned upgrade resolution
commit c6030fbf4d0c190f037c86d832edb6680d3e54ab
Author: Burak Yigit Kaya <byk@sentry.io>
AuthorDate: ...
e565b1afb39a341605adc883de8812851de7d581:packages/cli/test/commands/cli/upgrade.test.ts:175: if (urlStr === "https://api.github.com/repos/getsentry/toolkit") {
e565b1afb39a341605adc883de8812851de7d581:packages/cli/test/commands/cli/upgrade.test.ts:216: if (urlStr.includes("getsentry/toolkit/releases?per_page=100")) {
e565b1afb39a341605adc883de8812851de7d581:packages/cli/test/commands/cli/upgrade.test.ts:256: if (urlStr === "https://api.github.com/repos/getsentry/toolkit") {
e565b1afb39a341605adc883de8812851de7d581:packages/cli/test/commands/cli/upgrade.test.ts:364: request.includes("getsentry/toolkit/releases/tags/cli%4088.88.88")
e565b1afb39a341605adc883de8812851de7d581:packages/cli/test/commands/cli/upgrade.test.ts:368: if (request.includes("getsentry/cli/releases/tags/88.88.88")) {
e565b1afb39a341605adc883de8812851de7d581:packages/cli/test/commands/cli/upgrade.test.ts:373: if (request.includes("getsentry/cli/releases?per_page=30")) ...
1800 )
1801 ).toBe(false);
1802 });
1803 });
1804
1805 describe("startCleanupOldBinary", () => {
1806 // Get paths fresh to match what startCleanupOldBinary() uses
1807 function getOldPath() {
1808 return getCurlInstallPaths().oldPath;
1809 }
1810
1811 beforeEach(() => {
1812 // Clear any stored install info to ensure we use default paths
1813 clearInstallInfo();
1814 });
1815
1816 test("removes .old file if it exists", async () => {
1817 const oldPath = getOldPath();
1818 // Create the directory and file
1819 mkdirSync(join(oldPath, ".."), { recursive: true });
1820 writeFileSync(oldPath, "test content");
1821
1822 // Verify file exists
1823 expect(
1824 await access(oldPath).then(
1825 () => true,
1826 () => false
1827 )
1828 ).toBe(true);
1829
1830 // Clean up is fire-and-forget async, so we need to wait a bit
1831 ...
70 const log = logger.withTag("upgrade");
71
72 // Re-export for backward compatibility — consumers that import
73 // InstallationMethod from upgrade.ts continue to work.
74 export type { InstallationMethod } from "./binary.js";
75 // biome-ignore lint/performance/noBarrelFile: backward-compat re-export, not a barrel
76 export { parseInstallationMethod } from "./binary.js";
77
78 /** Package managers that can be used for global installs */
79 type PackageManager = "npm" | "pnpm" | "bun" | "yarn";
80
81 /**
82 * How the current upgrade reached the offline code path. ...
480 /**
481 * Fetch the latest version from GitHub releases.
482 *
483 * @param signal - Optional AbortSignal to cancel the request
484 * @returns Latest version string (without 'v' prefix)
485 * @throws {UpgradeError} When fetch fails or response is invalid
486 * @throws {Error} AbortError if signal is aborted
487 */
488 export async function fetchLatestFromGitHubWithSource(
489 signal?: AbortSignal,
490 sources: readonly UpgradeSource[] = UPGRADE_SOURCES
491 ): Promise<ResolvedUpgradeVersion> {
492 const resolved = await resolveUpgradeSource({
493 getProbeUrl: getGitHubLatestReleaseUrl,
494 signal,
495 sources,
496 });
497 let response = resolved.response;
498 const visitedPages = new Set([getGitHubLatestReleaseUrl(resolved.source)]);
499 const versions: string[] = [];
500 while (true) {
501 const data = (await response.json()) as
502 | { tag_name?: string }
503 ...
180 */
181 export type OciLayer = {
182 /** Content-addressable digest for the blob (e.g., "sha256:abc123...") */
183 digest: string;
184 /** MIME type of the layer content */
185 mediaType: string;
186 /** Size in bytes */
187 size: number;
188 /** Per-layer OCI annotations */
189 annotations?: Record<string, string>;
190 };
191
192 /**
193 * OCI image manifest returned by the registry.
194 *
195 * The `annotations` map at the manifest level holds metadata about the
196 * nightly push, including the `version` string baked in during `oras push`.
197 */
198 export type OciManifest = {
199 /** OCI manifest schema version (always 2) */
200 schemaVersion: number;
201 /** Manifest media type */
202 mediaType?: string;
203 /** Config layer (empty for ORAS artifacts) */
204 config?: OciLayer;
205 /** Content layers — one per binary/file pushed */
206 layers: OciLayer[];
...
e565b1afb39a341605adc883de8812851de7d581:packages/cli/src/commands/cli/upgrade.ts:692: executeUpgrade(method, target, downloadTag, offline, setMessage, source)
e565b1afb39a341605adc883de8812851de7d581:packages/cli/src/commands/cli/upgrade.ts:778: executeUpgrade("curl", target, downloadTag, undefined, setMessage, source)
e565b1afb39a341605adc883de8812851de7d581:packages/cli/src/lib/ghcr.ts:148:export const GHCR_REPO = PRIMARY_UPGRADE_SOURCE.ghcrRepo;
c3d7791d84ebb5ad57a949129d66f9147973d239432b7695c3f1d034a1956351 - 110d6530e57ca4fbfb2a3f98e52013d2668b5b5e d6c50e6e63351dd7e552c193199788fb455a5d47
e565b1afb39a341605adc883de8812851de7d581:packages/cli/src/commands/cli/setup.ts:39: setReleaseChannel,
e565b1afb39a341605adc883de8812851de7d581:packages/cli/src/commands/cli/setup.ts:513: setReleaseChannel(channel);
e565b1afb39a341605adc883de8812851de7d581:packages/cli/src/commands/cli/upgrade.ts:36: setReleaseChannel,
e565b1afb39a341605adc883de8812851de7d581:packages/cli/src/commands/cli/upgrade.ts:199: // Read the cached version BEFORE persisting the channel — setReleaseChannel
e565b1afb39a341605adc883de8812851de7d581:packages/cli/src/commands/cli/upgrade.ts:856: * target resolution since `setReleaseChannel()` clears the version check
e565b1afb39a341605adc883de8812851de7d581:packages/cli/src/commands/cli/upgrade.ts:866: setReleaseChannel(channel);
e565b1afb39a341605adc883de8812851de7d581:packages/cli/src/lib/db/release-channel.ts:45:export function setReleaseChannel(channel: ReleaseChannel): void {
780
781 if (downloadResult?.patchBytes) {
782 ...
485 const { flags, binaryPath, binaryDir, homeDir, env, emit, warn } = opts;
486 const shell = detectShell(env.SHELL, homeDir, env.XDG_CONFIG_HOME);
487
488 // 1. Record installation info
489 const method = flags.method;
490 if (method) {
491 await bestEffort(
492 "Recording installation info",
493 () => {
494 setInstallInfo({
495 method,
496 path: binaryPath,
497 version: CLI_VERSION,
498 });
499 if (!flags.install) {
500 emit(`Recorded installation method: ${method}`);
501 }
502 },
503 warn
504 );
505 }
506
507 // 1b. Persist release channel (set by install script or upgrade command)
508 const channel = flags.channel;
509 if (channel) {
510 await bestEffort(
511 "Recording release channel",
512 () => {
513 setReleaseChannel(channel);
514 if ...
836 async function resolveContext(
837 version: string | undefined,
838 flags: UpgradeFlags
839 ): Promise<{
840 channel: ReleaseChannel;
841 versionArg: string | undefined;
842 channelChanged: boolean;
843 method: InstallationMethod;
844 }> {
845 const { channel, versionArg } = resolveChannelAndVersion(version);
846 const currentChannel = getReleaseChannel();
847 const channelChanged = channel !== currentChannel;
848
849 const method = flags.method ?? (await detectInstallationMethod());
850 validateMethod(method, versionArg, channel, flags.offline);
851 return { channel, versionArg, channelChanged, method };
852 }
853
854 /**
855 * Persist the release channel preference. ...
1160 * is then a harmless no-op.
1161 *
1162 * @param version - Target version to download (used for display and comparison)
1163 * @param downloadTag - Git tag to use in the download URL. Defaults to `version`.
1164 * Pass `NIGHTLY_TAG` ("nightly") when installing from the rolling nightly release
1165 * so the URL points to the prerelease assets regardless of the version string.
1166 * @returns The downloaded binary path and lock path to release
1167 * @throws {UpgradeError} When download fails
1168 */
1169 // biome-ignore lint/nursery/useMaxParams: compatibility API; source preserves one selected repository across the download.
1170 export async function downloadBinaryToTemp(
1171 version: string,
1172 downloadTag?: string,
1173 offline?: OfflineMode,
1174 setMessage?: SetMessage,
1175 source: UpgradeSource = PRIMARY_UPGRADE_SOURCE
1176 ): Promise<DownloadResult> {
1177 const { tempPath, lockPath } = ...
e565b1afb39a341605adc883de8812851de7d581:packages/cli/src/lib/upgrade.ts:1309: * Runs `brew upgrade getsentry/tools/sentry` which fetches the latest
e565b1afb39a341605adc883de8812851de7d581:packages/cli/src/lib/upgrade.ts:1314: * @throws {UpgradeError} When brew upgrade fails
e565b1afb39a341605adc883de8812851de7d581:packages/cli/src/lib/upgrade.ts:1316:function executeUpgradeHomebrew(): Promise<void> {
e565b1afb39a341605adc883de8812851de7d581:packages/cli/src/lib/upgrade.ts:1330: `brew upgrade failed with exit code ${code}`
e565b1afb39a341605adc883de8812851de7d581:packages/cli/src/lib/upgrade.ts:1426: await executeUpgradeHomebrew();
Pinned-nightly Homebrew upgrades never reach standalone routing; the inverse path can install an unrequested Homebrew version.
/home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/toolkit-bridge-upgrade/packages/cli/src/commands/cli/upgrade.ts:256-262 rejects every Homebrew pin when the persisted channel is stable, including an exact nightly such as 0.99.0-dev.123. Execution therefore never reaches the intended target-based standalone branch at :1067-1077.:331-333 validates it through the standalone GitHub path, but :1078-1091 retains the Homebrew method. /home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/toolkit-bridge-upgrade/packages/cli/src/lib/upgrade.ts:1309-1318,1425-1427 then runs unpinned brew upgrade getsentry/tools/sentry. The command reports the requested pin at commands/cli/upgrade.ts:1094-1100 even though Homebrew installed its latest formula.Fix: classify the requested pin from the normalized target, not the persisted channel. Always route nightly pins through curl/standalone. Always reject stable Homebrew pins before network or subprocess work. Also choose lookupMethod from isNightlyVersion(target), not channel === "nightly".
Deterministic regressions:
upgrade --method brew 0.99.0-dev.123, and assert GHCR resolution plus standalone installation; assert brew is never spawned.upgrade --method brew 1.2.3, and assert unsupported_operation before any fetch or spawn.* 🔴 (03:25) User showed setup behavior in `packages/cli/src/commands/cli/setup.ts:485-525`: `detectShell(env.SHELL, homeDir, env.XDG_CONFIG_HOME)` runs first; when `flags.method` exists, `bestEffort("Recording installation info", ...)` calls `setInstallInfo({ method, path: binaryPath, version: CLI_VERSION })` and emits `Recorded installation method: ${method}` only when `!flags.install`; when `f…
* 🔴 (03:23) User stated package managers always need network access to fetch and install packages; consequently, `validateMethod()` in `packages/cli/src/commands/cli/upgrade.ts` permits offline mode only for `method === "curl"` and otherwise throws `UpgradeError("unsupported_operation", "Offline upgrade is only supported for curl-installed binaries.")`. * 🔴 (03:23) User showed `fetchLatestFromG…
Date: Sep 10, 2026 * 🔴 (03:22) User showed branch `feat/toolkit-bridge-upgrade` tracking `origin/feat/toolkit-bridge-upgrade`, with `e565b1afb39a341605adc883de8812851de7d581` at `HEAD`. * 🔴 (03:22) User showed Sep 10 commit history by Burak Yigit Kaya `<byk@sentry.io>` in newest-to-oldest order: `e565b1afb39a341605adc883de8812851de7d581` — `fix(cli): validate stable upgrade metadata`; `4a1d5bad…
* 🔴 (03:20) User showed `packages/cli/test/lib/release-notes.test.ts` expanding Vitest imports to `afterEach`, `beforeEach`, `describe`, `expect`, and `test`; importing `UPGRADE_SOURCES` from `../../src/lib/binary.js`, `fetchRecentReleases` and `GitHubRelease` from `../../src/lib/delta-upgrade.js`, `fetchChangelog` from `../../src/lib/release-notes.js`, and `mockFetch` from `../helpers.js`. * 🔴…
* 🔴 (03:20) User showed `packages/cli/test/lib/binary.test.ts` updating `getBinaryDownloadUrl("1.0.0")` expectations from the legacy `https://github.com/getsentry/cli/releases/download/` path containing `/1.0.0/` to the Toolkit path `https://github.com/getsentry/toolkit/releases/download/` containing `/cli@1.0.0/`; the URL must still contain `sentry-` and use `arm64` when `process.arch === "arm6…
* 🔴 (03:20) User stated switching to the nightly release channel while using the npm installation method triggers migration to a standalone installation. * 🔴 (03:20) User provided an upgrade migration test expecting these messages: `"Nightly builds are only available as standalone binaries."`, `"Migrating to standalone installation..."`, `"Upgraded to"`, `"npm-installed sentry may still appear …
* 🔴 (03:19) User stated `packages/cli/src/lib/release-notes.ts` now imports `valid as semverValid` from `semver` and imports `getGitHubReleasesUrl`, `PRIMARY_UPGRADE_SOURCE`, and `type UpgradeSource` from `./binary.js`, replacing direct use of `GITHUB_RELEASES_URL`. * 🔴 (03:19) User stated release-note aggregation supports a selected `UpgradeSource`; `FetchChangelogOptions` includes `source?: U…
Date: Sep 10, 2026 * 🔴 (03:18) User stated `packages/cli/src/lib/ghcr.ts` now imports `valid as semverValid` from `semver` and imports `PRIMARY_UPGRADE_SOURCE` plus `type UpgradeSource` from `./binary.js`. * 🔴 (03:18) User stated `getAnonymousToken(sourceOrSignal: UpgradeSource | AbortSignal = PRIMARY_UPGRADE_SOURCE, signal?: AbortSignal)` supports either an upgrade source or the legacy `AbortS…
* 🔴 (03:18) User specified `detectInstallationMethod()` in `packages/cli/src/lib/upgrade.ts` must always check Homebrew first because stored install information may be stale; a cheap, authoritative `isHomebrewInstall()` realpath check overrides stale stored data, such as when a user switches from curl installation to Homebrew. * 🔴 (03:18) User specified installation-method detection priority: 1…
Date: Sep 10, 2026 * 🔴 [requested-review] (03:17) User requested a READ-ONLY security/supply-chain merge-gate review of getsentry/cli PR #1569 in repository `/home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/toolkit-bridge-upgrade`, at exact base `ec83887a16f780f32fba4b7d710bad262dba3a22` and head `e565b1afb39a341605adc883de8812851de7d581`. * 🔴 (03:17) User direc…