Dashboard › cli › Session 14GgsRiWwjbI
14GgsRiWwjbIayfJtYou are the replacement READ-ONLY security and supply-chain merge-gate reviewer for getsentry/cli PR #1569. The prior reviewer returned empty twice. A substantive evidence-based response is mandatory. Repository: /home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/toolkit-bridge-upgrade. Review exact immutable base ec83887a16f780f32fba4b7d710bad262dba3a22 and exact immutable head 9525f72c70fabbb23ec21d13cf86add58f6287cc using Git objects only. Never inspect or modify the worktree, mutate VCS, or write files. Verify merge-base/base ancestry, base/head trees, changed paths, full diff, and git diff --check. Inspect every changed source and test object. Adversarially verify: Toolkit-first fallback advances only on genuine typed HTTP 404; non-404 HTTP, transport, TLS, malformed success never fall through; successful source sticks through stable/nightly exact/latest lookup, token scope, manifests, blobs, delta, prefetch, changelog and setup; GitHub pagination reconstructs trusted selected-repo URLs and detects cycles; response-body cancellation preserves exact arbitrary signal.reason, body termination remains UpgradeTransportError, malformed complete JSON is terminal metadata; npm latest JSON has strict runtime shape and stable canonical SemVer; pinned npm/pnpm/bun/yarn 404 vs 401/403/429/500 classification uses one request and never launches subprocess on failure; Toolkit accepts only cli@VERSION while legacy supports vVERSION; malformed/prerelease standalone pins and draft/prerelease exact release metadata fail before download/setup; GHCR tokens are trimmed non-empty strings; OCI manifests/layers/digests/sizes/annotations and tag-list bodies validate at runtime; annotation arrays fail; tag pagination detects repeated cursor; bearer auth never reaches redirect; nightly annotation binds exact target; artifact channel and tracking channel remain separate; offline behavior and transport-only cache fallback; delta application final SHA ...
/home/byk/Code/getsentry/cli/.git/worktrees/toolkit-bridge-upgrade commit commit BASE commit=ec83887a16f780f32fba4b7d710bad262dba3a22 tree=110d6530e57ca4fbfb2a3f98e52013d2668b5b5e parents=e7beceb6ba09b64dc277ae1732df04e65500062b BASE subject=fix(event): Prevent immediate retry of rate-limited org in event view fallback (#1566) HEAD commit=9525f72c70fabbb23ec21d13cf86add58f6287cc tree=22f49363a13e32c36dcd9e1ca95916809c2dc271 parents=a84012184c79c2566c7466aa6beda5f767199218 HEAD subject=fix(cli): harden upgrade metadata validation merge-base=ec83887a16f780f32fba4b7d710bad262dba3a22 base-is-ancestor=yes
CHANGED PATHS 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/errors.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 DIFF STAT packages/cli/src/commands/cli/upgrade.ts | 180 +++-- packages/cli/src/lib/binary.ts | 191 +++++- packages/cli/src/lib/delta-upgrade.ts | 170 ++++- packages/cli/src/lib/errors.ts | 8 + packages/cli/src/lib/ghcr.ts ...
diff --git a/packages/cli/src/commands/cli/upgrade.ts b/packages/cli/src/commands/cli/upgrade.ts
index 8d8282368..b3039d86b 100644
--- a/packages/cli/src/commands/cli/upgrade.ts
+++ b/packages/cli/src/commands/cli/upgrade.ts
@@ -22,9 +22,11 @@ import type { SentryContext } from "../../context.js";
import {
determineInstallDir,
isDowngrade,
+ isNightlyVersion,
LEGACY_INSTALL_SUBDIR,
releaseLock,
samePath,
+ type UpgradeSource,
} from "../../lib/binary.js";
import { buildCommand } from "../../lib/command.js";
import { CLI_VERSION } from "../../lib/constants.js";
@@ -34,7 +36,7 @@ import {
setReleaseChannel,
} from "../../lib/db/release-channel.js";
import { getVersionCheckInfo } from "../../lib/db/version-check.js";
-import { UpgradeError } from "../../lib/errors.js";
+import { UpgradeError, UpgradeTransportError } from "../../lib/errors.js";
import { formatUpgradeResult } from "../../lib/formatters/human.js";
import { formatBytes } from ...
diff --git a/packages/cli/src/lib/binary.ts b/packages/cli/src/lib/binary.ts
index 75582612a..e64f5059e 100644
--- a/packages/cli/src/lib/binary.ts
+++ b/packages/cli/src/lib/binary.ts
@@ -23,7 +23,11 @@ import {
customFetch,
isTlsCertError,
} from "./custom-ca.js";
-import { stringifyUnknown, UpgradeError } from "./errors.js";
+import {
+ stringifyUnknown,
+ UpgradeError,
+ UpgradeTransportError,
+} from "./errors.js";
import { logger } from "./logger.js";
import { isProcessRunning } from "./process-utils.js";
/** Known directories where the curl installer may place the binary */
@@ -102,6 +106,33 @@ export type InstallationMethod =
| "yarn"
| "unknown";
+/** A repository pair that hosts CLI stable releases and nightly OCI images. */
+export type UpgradeSource = {
+ /** GitHub `owner/repository` containing CLI release assets. */
+ readonly githubRepo: string;
+ /** GHCR `owner/package` containing CLI nightly images and delta patches. ...
diff --git a/packages/cli/src/lib/delta-upgrade.ts b/packages/cli/src/lib/delta-upgrade.ts
index ec709b8c8..204875dce 100644
--- a/packages/cli/src/lib/delta-upgrade.ts
+++ b/packages/cli/src/lib/delta-upgrade.ts
@@ -30,18 +30,20 @@ import {
type SourceStrategy,
type StableChainInfo,
} from "binpatch";
+import { prerelease as semverPrerelease, valid as semverValid } from "semver";
import {
compareVersions,
- GITHUB_RELEASES_URL,
+ getGitHubReleasesUrl,
getPlatformBinaryName,
isDowngrade,
isNightlyVersion,
+ PRIMARY_UPGRADE_SOURCE,
+ type UpgradeSource,
} from "./binary.js";
import { CLI_VERSION } from "./constants.js";
import { customFetch } from "./custom-ca.js";
import { getConfigDir } from "./db/index.js";
import { formatBytes } from "./formatters/numbers.js";
-import { GHCR_REPO } from "./ghcr.js";
import { logger } from "./logger.js";
import { makeByteProgress, type SetMessage } from "./progress.js";
import { withTracing, withTracingSpan } from ...
diff --git a/packages/cli/src/lib/errors.ts b/packages/cli/src/lib/errors.ts
index f81b8c70c..b21505cd1 100644
--- a/packages/cli/src/lib/errors.ts
+++ b/packages/cli/src/lib/errors.ts
@@ -618,6 +618,14 @@ export class UpgradeError extends CliError {
}
}
+/** Upgrade failure caused by transport rather than an HTTP or metadata error. */
+export class UpgradeTransportError extends UpgradeError {
+ constructor(message: string) {
+ super("network_error", message);
+ this.name = "UpgradeTransportError";
+ }
+}
+
// Seer Errors
export type SeerErrorReason = "not_enabled" | "no_budget" | "ai_disabled";
diff --git a/packages/cli/src/lib/ghcr.ts b/packages/cli/src/lib/ghcr.ts
index 43d6deb7e..5bca7c061 100644
--- a/packages/cli/src/lib/ghcr.ts
+++ b/packages/cli/src/lib/ghcr.ts
@@ -17,9 +17,15 @@
* without the auth header.
*/
+import { valid as semverValid } from "semver";
+import {
+ PRIMARY_UPGRADE_SOURCE,
+ parseUpgradeJson,
+ type UpgradeSource,
+} from "./binary.js";
import { getUserAgent } from "./constants.js";
import { customFetch } from "./custom-ca.js";
-import { UpgradeError } from "./errors.js";
+import { UpgradeError, UpgradeTransportError } from "./errors.js";
/** Default timeout for GHCR HTTP requests (10 seconds) */
const GHCR_REQUEST_TIMEOUT = 10_000;
@@ -27,6 +33,9 @@ const GHCR_REQUEST_TIMEOUT = 10_000;
/** Maximum number of retry attempts for transient failures */
const GHCR_MAX_RETRIES = 1;
+/** Nightly versions use a numeric build timestamp as the prerelease value. */
+const NIGHTLY_VERSION_REGEX = /^\d+\.\d+\.\d+-dev\.\d+$/;
+
/** Timeout ...
diff --git a/packages/cli/src/lib/release-notes.ts b/packages/cli/src/lib/release-notes.ts
index ddf617535..903ca51f6 100644
--- a/packages/cli/src/lib/release-notes.ts
+++ b/packages/cli/src/lib/release-notes.ts
@@ -15,11 +15,18 @@
import { marked, type Token, type Tokens } from "marked";
import {
compareVersions,
- GITHUB_RELEASES_URL,
getGitHubHeaders,
+ getGitHubReleasesUrl,
+ PRIMARY_UPGRADE_SOURCE,
+ type UpgradeSource,
} from "./binary.js";
import { customFetch } from "./custom-ca.js";
-import type { GitHubRelease } from "./delta-upgrade.js";
+import {
+ type GitHubRelease,
+ isNormalizedForSource,
+ type NormalizedGitHubReleases,
+ normalizeStableReleases,
+} from "./delta-upgrade.js";
import { logger } from "./logger.js";
const log = logger.withTag("release-notes");
@@ -413,27 +420,34 @@ function mergeSectionsByCategory(releases: GitHubRelease[]): ChangeSection[] {
return merged;
}
-/**
- * Build a changelog summary from a list of GitHub releases. ...
diff --git a/packages/cli/src/lib/upgrade.ts b/packages/cli/src/lib/upgrade.ts
index 945adc48d..b29d67ff2 100644
--- a/packages/cli/src/lib/upgrade.ts
+++ b/packages/cli/src/lib/upgrade.ts
@@ -21,35 +21,47 @@ 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,
determineInstallDir,
fetchWithUpgradeError,
- GITHUB_RELEASES_URL,
getBinaryDownloadUrl,
getBinaryFilename,
getBinaryPaths,
getGitHubHeaders,
+ getGitHubLatestReleaseUrl,
+ getGitHubReleaseByTagUrl,
+ getGitHubRepositoryUrl,
getPlatformBinaryName,
type InstallationMethod,
isNightlyVersion,
KNOWN_CURL_DIRS,
+ PRIMARY_UPGRADE_SOURCE,
+ parseUpgradeJson,
releaseLock,
+ resolveUpgradeSource,
+ ...
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
@@ -10,6 +10,7 @@
// 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 {
@@ -27,7 +28,10 @@ import { cyan, muted } from "./formatters/colors.js";
import { GLOBAL_FLAGS } from "./global-flags.js";
import { logger } from "./logger.js";
import { cleanupPatchCache } from "./patch-cache.js";
-import { fetchLatestFromGitHub, fetchLatestNightlyVersion } from "./upgrade.js";
+import {
+ fetchLatestFromGitHubWithSource,
+ fetchLatestNightlyVersionWithSource,
+} from ...
diff --git a/packages/cli/test/commands/cli.test.ts b/packages/cli/test/commands/cli.test.ts
index cd1514eb0..4eec11e9d 100644
--- a/packages/cli/test/commands/cli.test.ts
+++ b/packages/cli/test/commands/cli.test.ts
@@ -124,7 +124,7 @@ describe("upgradeCommand.func", () => {
test("shows installation info with specified method", async () => {
globalThis.fetch = (async () =>
- new Response(JSON.stringify({ tag_name: "v0.0.0-dev" }), {
+ new Response(JSON.stringify([{ tag_name: "cli@1.0.0" }]), {
status: 200,
headers: { "Content-Type": "application/json" },
})) as typeof fetch;
@@ -135,18 +135,18 @@ describe("upgradeCommand.func", () => {
// Use method flag to bypass detection (curl uses GitHub).
// Pass json: true so the output config renders structured JSON to stdout.
- await func.call(context, { check: false, method: "curl", json: true });
+ await func.call(context, { check: true, method: "curl", json: true });
// ...
diff --git a/packages/cli/test/commands/cli/upgrade.test.ts b/packages/cli/test/commands/cli/upgrade.test.ts
index ca9688e46..b7fead58f 100644
--- a/packages/cli/test/commands/cli/upgrade.test.ts
+++ b/packages/cli/test/commands/cli/upgrade.test.ts
@@ -40,6 +40,7 @@ import {
getReleaseChannel,
setReleaseChannel,
} from "../../../src/lib/db/release-channel.js";
+import { setVersionCheckInfo } from "../../../src/lib/db/version-check.js";
import { TEST_TMP_DIR, useTestConfigDir } from "../../helpers.js";
/** Store original fetch for restoration */
@@ -172,6 +173,10 @@ function mockGhcrNightlyVersion(version: string): void {
mockFetch(async (url) => {
const urlStr = String(url);
+ if (urlStr === "https://api.github.com/repos/getsentry/toolkit") {
+ return new Response(null, { status: 200 });
+ }
+
// GHCR anonymous token exchange
if (urlStr.includes("ghcr.io/token")) {
return new Response(JSON.stringify({ token: "test-token" }), {
@@ ...
diff --git a/packages/cli/test/lib/binary.test.ts b/packages/cli/test/lib/binary.test.ts
index 6fcfb3b1a..29ead3828 100644
--- a/packages/cli/test/lib/binary.test.ts
+++ b/packages/cli/test/lib/binary.test.ts
@@ -25,14 +25,19 @@ import {
getBinaryDownloadUrl,
getBinaryFilename,
getBinaryPaths,
+ getGitHubReleaseByTagUrl,
getLegacyInstallDirs,
getPlatformBinaryName,
installBinary,
isDowngrade,
isMusl,
+ parseUpgradeJson,
releaseLock,
replaceBinarySync,
+ resolveUpgradeSource,
samePath,
+ UPGRADE_SOURCES,
+ UpgradeSourceNotFoundError,
} from "../../src/lib/binary.js";
import { UpgradeError } from "../../src/lib/errors.js";
@@ -40,9 +45,9 @@ describe("getBinaryDownloadUrl", () => {
test("builds correct URL for current platform", () => {
const url = getBinaryDownloadUrl("1.0.0");
- expect(url).toContain("/1.0.0/");
+ expect(url).toContain("/cli@1.0.0/");
expect(url).toStartWith(
- ...
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
@@ -99,10 +99,10 @@ describe("resolveStableDelta", () => {
// Set up fetch mocks — releases API + patch download
// Since applyPatch will fail (we don't have a real TRDIFF10 matching this binary),
// we expect resolveStableDelta to throw, but the chain resolution should succeed
- const patchUrl = `https://github.com/getsentry/cli/releases/download/0.14.0/${BINARY_NAME}.patch`;
+ const patchUrl = `https://github.com/getsentry/toolkit/releases/download/cli@0.14.0/${BINARY_NAME}.patch`;
const releases = [
{
- tag_name: "0.14.0",
+ tag_name: "cli@0.14.0",
assets: [
{
name: BINARY_NAME,
@@ -123,7 +123,7 @@ describe("resolveStableDelta", () => {
...
diff --git a/packages/cli/test/lib/delta-upgrade.test.ts b/packages/cli/test/lib/delta-upgrade.test.ts
index 3752d4ae6..68d1cfbdf 100644
--- a/packages/cli/test/lib/delta-upgrade.test.ts
+++ b/packages/cli/test/lib/delta-upgrade.test.ts
@@ -11,8 +11,11 @@ import { existsSync, unlinkSync } from "node:fs";
import { access, readFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
-import { afterEach, beforeEach, describe, expect, test } from "vitest";
-import { getPlatformBinaryName } from "../../src/lib/binary.js";
+import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
+import {
+ getPlatformBinaryName,
+ UPGRADE_SOURCES,
+} from "../../src/lib/binary.js";
import {
applyPatchChain,
attemptDeltaUpgrade,
@@ -38,6 +41,12 @@ import {
validateChainStep,
} from "../../src/lib/delta-upgrade.js";
import type { OciManifest } from "../../src/lib/ghcr.js";
+import { useTestConfigDir } from ...
diff --git a/packages/cli/test/lib/ghcr.test.ts b/packages/cli/test/lib/ghcr.test.ts
index e5c81d532..a55612b5a 100644
--- a/packages/cli/test/lib/ghcr.test.ts
+++ b/packages/cli/test/lib/ghcr.test.ts
@@ -6,6 +6,7 @@
*/
import { afterEach, beforeEach, describe, expect, test } from "vitest";
+import { UPGRADE_SOURCES } from "../../src/lib/binary.js";
import { UpgradeError } from "../../src/lib/errors.js";
import {
downloadLayerBlob,
@@ -15,6 +16,7 @@ import {
findLayerByFilename,
GHCR_REPO,
GHCR_TAG,
+ GhcrManifestHttpError,
getAnonymousToken,
getNightlyVersion,
listTags,
@@ -37,13 +39,13 @@ function makeManifest(overrides: Partial<OciManifest> = {}): OciManifest {
schemaVersion: 2,
mediaType: "application/vnd.oci.image.manifest.v1+json",
config: {
- digest: "sha256:config",
+ digest: `sha256:${"0".repeat(64)}`,
mediaType: "application/vnd.oci.empty.v1+json",
size: 2,
},
layers: [
{
- digest: ...
diff --git a/packages/cli/test/lib/release-notes.test.ts b/packages/cli/test/lib/release-notes.test.ts
index 78963b471..ffccf4901 100644
--- a/packages/cli/test/lib/release-notes.test.ts
+++ b/packages/cli/test/lib/release-notes.test.ts
@@ -9,16 +9,22 @@
*/
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 "../../src/lib/delta-upgrade.js";
import {
buildChangelogSummary,
type ChangeCategory,
countListItems,
extractNightlyTimestamp,
extractSections,
+ fetchChangelog,
parseCommitMessages,
} from "../../src/lib/release-notes.js";
+import { mockFetch } from "../helpers.js";
// ─────────────────────────── Fixtures ──────────────────────────────────────
@@ -293,3 ...
diff --git a/packages/cli/test/lib/upgrade.test.ts b/packages/cli/test/lib/upgrade.test.ts
index 7c6f63a4b..d10f21dd6 100644
--- a/packages/cli/test/lib/upgrade.test.ts
+++ b/packages/cli/test/lib/upgrade.test.ts
@@ -120,6 +120,7 @@ import {
getBinaryDownloadUrl,
isNightlyVersion,
releaseLock,
+ UPGRADE_SOURCES,
} from "../../src/lib/binary.js";
import {
clearInstallInfo,
@@ -140,6 +141,7 @@ const {
fetchLatestVersion,
getCurlInstallPaths,
parseInstallationMethod,
+ resolveExistingUpgradeVersion,
startCleanupOldBinary,
versionExists,
} = await import("../../src/lib/upgrade.js");
@@ -188,37 +190,234 @@ describe("parseInstallationMethod", () => {
});
describe("fetchLatestFromGitHub", () => {
- test("returns version from GitHub API", async () => {
- mockFetch(
- async () =>
- new Response(
- JSON.stringify({
- tag_name: "v1.2.3",
- }),
+ test("selects the latest CLI-prefixed Toolkit release", async () => ...
diff --git a/packages/cli/test/lib/version-check.test.ts b/packages/cli/test/lib/version-check.test.ts
index 20172cd83..dd047a5cb 100644
--- a/packages/cli/test/lib/version-check.test.ts
+++ b/packages/cli/test/lib/version-check.test.ts
@@ -3,12 +3,15 @@
*/
import { setTimeout as sleep } from "node:timers/promises";
-import { afterEach, beforeEach, describe, expect, test } from "vitest";
+import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
+import { UPGRADE_SOURCES } from "../../src/lib/binary.js";
import { setReleaseChannel } from "../../src/lib/db/release-channel.js";
import {
getVersionCheckInfo,
setVersionCheckInfo,
} from "../../src/lib/db/version-check.js";
+// biome-ignore lint/performance/noNamespaceImport: Vitest requires the module namespace to spy on an ESM export
+import * as deltaUpgrade from "../../src/lib/delta-upgrade.js";
import {
ApiError,
ContextError,
@@ -440,6 +443,7 @@ describe("maybeCheckForUpdateInBackground", () => ...
COMMITS BASE..HEAD 04867eaf9b412a62d4b1ea1c59c2b382c557bd0b cc356b77799633918710e0c8d358034894f86147 ec83887a16f780f32fba4b7d710bad262dba3a22 feat(cli): add toolkit upgrade bridge 9c874298e3b57515ba36d35140627a668a46dc7b 4dd2c6153592d15804b1aaef8da9015e0df0345b 04867eaf9b412a62d4b1ea1c59c2b382c557bd0b fix(cli): address toolkit bridge review 723375b6ce4e37884e760fa0f848e7e7e3effcc9 c0c0ace97578535769b33929fb2494113eec3e5d 9c874298e3b57515ba36d35140627a668a46dc7b fix(cli): address upgrade review findings dbee7e9f2a4555e044f74dfebad002cccfa43a1f 024dbc7afa15d0632eb4c15ed6c808dab974a9c5 723375b6ce4e37884e760fa0f848e7e7e3effcc9 test(cli): update toolkit upgrade fixtures 5ffa5cd0fe02c0a5dc5bec9a8a099f4135c0b447 a1cbad382fe2fec2c40f31bd64eaf1898b779428 dbee7e9f2a4555e044f74dfebad002cccfa43a1f fix(cli): harden upgrade source resolution c8c815bbecf472061ec02ab96d1a31f23f078d01 ecd342742030111dd72ce73739016ac3b803d521 5ffa5cd0fe02c0a5dc5bec9a8a099f4135c0b447 fix(cli): validate GitHub release ...
1114 lines 37246 bytes b3039d86b5935a806f265c906460686d83ee2c64 packages/cli/src/commands/cli/upgrade.ts 768 lines 25572 bytes e64f5059e53d1321c3910ba46ead18391343348f packages/cli/src/lib/binary.ts 725 lines 21916 bytes 204875dce4325434ff6c4b7b0fcc4d2abb9bbd50 packages/cli/src/lib/delta-upgrade.ts 1012 lines 34040 bytes b21505cd11bdfddf7c51f0b89d942cc850ea29fe packages/cli/src/lib/errors.ts 659 lines 20203 bytes 5bca7c0615a2cdd1ee956e88259d4d046e3bc97b packages/cli/src/lib/ghcr.ts 793 lines 25097 bytes 903ca51f67706a30a1e203ff1df3b288018ed3d5 packages/cli/src/lib/release-notes.ts 1502 lines 48462 bytes b29d67ff2da3f1ba4940ec971aa9b11f41ed390f packages/cli/src/lib/upgrade.ts 501 lines 16463 bytes 34393b1c777e09c0a1ab683a493a6f15d91811c3 packages/cli/src/lib/version-check.ts 230 lines 7592 bytes 4eec11e9d8fa20ef1c55738a82df0a7f3bbad6bd packages/cli/test/commands/cli.test.ts 1584 lines 50894 bytes b7fead58fbef43936ca22d8e4901000961ce740d ...
1 /**
2 * sentry cli upgrade
3 *
4 * Self-update the Sentry CLI to the latest or a specific version.
5 * After upgrading, spawns the NEW binary with `cli setup` to update
6 * completions, agent skills, and record installation metadata.
7 *
8 * Supports two release channels:
9 * - stable (default): tracks the latest GitHub release
10 * - nightly: tracks the rolling nightly prerelease built from main
11 *
12 * The channel can be set via --channel or by passing "nightly"/"stable"
13 * as the version argument. The choice is persisted in the local database
14 * so that subsequent bare `sentry cli upgrade` calls use the same channel.
15 */
16
17 import { spawn } from "node:child_process";
18 import { homedir } from "node:os";
19 import { dirname, join } from "node:path";
20 import { setTimeout } from "node:timers/promises";
21 import type { SentryContext } from "../../context.js";
...
401 currentVersion: CLI_VERSION,
402 targetVersion: target,
403 channel,
404 method,
405 forced: flags.force,
406 };
407
408 // When already on target, no update hint needed
409 if (CLI_VERSION !== target) {
410 const cmd =
411 versionArg && !CHANNEL_VERSIONS.has(versionArg)
412 ? `sentry cli upgrade ${target}`
413 : "sentry cli upgrade";
414 result.warnings = [`Run '${cmd}' to update.`];
415 }
416
417 return result;
418 }
419
420 /**
421 * Maximum number of spawn attempts for the new binary.
422 *
423 * On Windows, Defender/SmartScreen may lock a newly-written executable for
424 * antivirus scanning after the file handle is closed. This causes EBUSY from
425 * uv_spawn. Retrying with backoff lets the scan complete without a fixed sleep. ...
801 try {
802 await runSetupOnNewBinary({
803 binaryPath: downloadResult.tempBinaryPath,
804 method: "curl",
805 channel,
806 install: true,
807 installDir,
808 ensureAuthScopes: !json,
809 noAgentSkills,
810 });
811 } finally {
812 releaseLock(downloadResult.lockPath);
813 }
814
815 // Build warnings about the potentially shadowing old installation.
816 // Note: install info is already recorded by the child `setup --install`
817 // process, so no redundant setInstallInfo call is needed here.
818 const uninstallHints: Record<string, string> = {
819 npm: "npm uninstall -g sentry",
820 pnpm: "pnpm remove -g sentry",
821 bun: "bun remove -g sentry",
822 yarn: "yarn global remove sentry",
823 brew: "brew uninstall getsentry/tools/sentry",
824 };
825 const warnings: string[] = [];
826 warnings.push(
827 `Your ...
1 /**
2 * Binary Management
3 *
4 * Shared utilities for installing, replacing, and managing the CLI binary.
5 * Used by both `setup --install` (fresh installs) and `upgrade` (self-updates).
6 */
7
8 import { spawnSync } from "node:child_process";
9 import {
10 existsSync,
11 mkdirSync,
12 readFileSync,
13 renameSync,
14 unlinkSync,
15 writeFileSync,
16 } from "node:fs";
17 import { chmod, copyFile, mkdir, realpath, unlink } from "node:fs/promises";
18 import { delimiter, dirname, isAbsolute, join, resolve, sep } from "node:path";
19 import { compare as semverCompare } from "semver";
20 import { getUserAgent } from "./constants.js";
21 import {
22 buildTlsErrorDetail,
23 customFetch,
24 isTlsCertError,
25 } from "./custom-ca.js";
26 import {
27 stringifyUnknown,
28 UpgradeError,
29 UpgradeTransportError,
30 } from ...
401 * @returns "sentry.exe" on Windows, "sentry" elsewhere
402 */
403 export function getBinaryFilename(): string {
404 return process.platform === "win32" ? "sentry.exe" : "sentry";
405 }
406
407 /**
408 * Build paths object from an install path.
409 * Returns the install path and derived sibling paths used during
410 * download, replacement, and locking.
411 *
412 * @param installPath - Absolute path to the binary
413 * @returns Object with install, temp (.download), old (.old), and lock (.lock) paths
414 */
415 export function getBinaryPaths(installPath: string): {
416 installPath: string;
417 tempPath: string;
418 oldPath: string;
419 lockPath: string;
420 } {
421 return {
422 installPath,
423 tempPath: `${installPath}.download`,
424 oldPath: `${installPath}.old`,
425 lockPath: `${installPath}.lock`,
426 };
427 }
428
429 /**
430 * Determine the ...
1 /** Delta upgrade discovery and application backed by binpatch. */
2
3 import { join } from "node:path";
4 // biome-ignore lint/performance/noNamespaceImport: Sentry SDK recommends namespace import
5 import * as Sentry from "@sentry/node-core/light";
6 import {
7 applyPatchChainInMemory,
8 extractStableChain as binpatchExtractStableChain,
9 filterAndSortChainTags as binpatchFilterAndSortChainTags,
10 validateChainStep as binpatchValidateChainStep,
11 type DeltaTelemetry,
12 type DeltaUnavailableReason,
13 type ExtractStableChainOpts,
14 type GitHubRelease,
15 getPatchFromVersion,
16 getPatchTargetSha256,
17 ghcrSource,
18 githubReleaseSource,
19 type InstrumentHook,
20 MAX_NIGHTLY_CHAIN_DEPTH,
21 makeCache,
22 OciClient,
23 type OciManifest,
24 PATCH_TAG_PREFIX,
25 type PatchCache,
26 type PatchChain,
27 type ...
381 repo: source.ghcrRepo,
382 userAgent: `sentry-cli/${CLI_VERSION}`,
383 fetch: customFetch,
384 });
385 const tags =
386 opts.preloadedTags ??
387 (await client.listTags(opts.token, PATCH_TAG_PREFIX, opts.signal));
388 const chainTags = filterAndSortChainTags(
389 tags,
390 opts.currentVersion,
391 opts.targetVersion
392 );
393 if (chainTags.length === 0 || chainTags.length > MAX_NIGHTLY_CHAIN_DEPTH) {
394 return null;
395 }
396
397 let manifests: OciManifest[];
398 // biome-ignore lint/plugin: grandfathered silent catch — see #1531; drain by adding log.debug()/log.warn() or re-throwing.
399 try {
400 manifests = await Promise.all(
401 chainTags.map((tag) => client.fetchManifest(opts.token, tag, opts.signal))
402 );
403 } catch {
404 return null;
405 }
406 const binaryName = getPlatformBinaryName();
407 const ...
1 /**
2 * CLI Error Hierarchy
3 *
4 * Unified error classes for consistent error handling across the CLI.
5 *
6 * ## Exit Code Ranges
7 *
8 * Each error class maps to a semantic exit code so scripts and agents can
9 * react to failure categories without parsing stderr. Codes are grouped
10 * into decades inspired by HTTP status semantics:
11 *
12 * | Range | Category | HTTP Analogy |
13 * |-------|-------------------|----------------------|
14 * | 0 | Success | 200 OK |
15 * | 1 | General error | 500 Internal |
16 * | 10–19 | Auth & identity | 401/403 |
17 * | 20–29 | Input & config | 400/404/422 |
18 * | 30–39 | API & network | 502/503/504 |
19 * | 40–49 | Feature/billing | 402/451 |
20 * | 50–59 | Operations | — |
21 * | 60–69 | ...
1 /**
2 * GHCR (GitHub Container Registry) Client
3 *
4 * Encapsulates the OCI download protocol for fetching nightly CLI binaries
5 * from ghcr.io/getsentry/cli. Nightly builds are pushed as OCI artifacts
6 * via ORAS with the version baked into the manifest annotation.
7 *
8 * Key design decisions:
9 * - Anonymous access: nightly package is public; no token needed beyond the
10 * standard ghcr.io anonymous token exchange.
11 * - Version discovery from manifest annotation: `annotations.version` in the
12 * OCI manifest holds the nightly version. Checking the latest version only
13 * requires a token exchange + manifest fetch (2 HTTP requests total).
14 * - Redirect quirk: ghcr.io blob downloads return 307 to Azure Blob Storage.
15 * Using `fetch` with `redirect: "follow"` would forward the Authorization
16 * header to Azure, which returns 404. ...
351 * Convenience wrapper around {@link fetchManifest} for the rolling nightly tag.
352 *
353 * @param token - Anonymous bearer token from {@link getAnonymousToken}
354 * @returns Parsed OCI manifest
355 * @throws {UpgradeError} On network failure or non-200 response
356 */
357 export async function fetchNightlyManifest(
358 token: string,
359 signal?: AbortSignal,
360 source: UpgradeSource = PRIMARY_UPGRADE_SOURCE
361 ): Promise<OciManifest> {
362 return await fetchManifest(token, GHCR_TAG, signal, source);
363 }
364
365 /**
366 * Extract the nightly version string from a manifest's annotations.
367 *
368 * The version is set via `--annotation "version=<ver>"` during `oras push`.
369 *
370 * @param manifest - OCI manifest from {@link fetchNightlyManifest}
371 * @returns Version string (e.g., "0.13.0-dev.1740000000")
372 * @throws {UpgradeError} When the version annotation is missing
373 */
...
1 /**
2 * Release Notes Parser & Aggregation
3 *
4 * Extracts user-facing changelog entries from GitHub Release bodies (stable)
5 * or conventional commit messages (nightly). Uses `marked.lexer()` for
6 * AST-based section extraction and produces structured data that can be
7 * re-serialized as filtered markdown for rendering via `renderMarkdown()`.
8 *
9 * Only three categories are kept — everything else is filtered out:
10 * - **New Features** (✨) — from `### New Features` sections or `feat:` commits
11 * - **Bug Fixes** (🐛) — from `### Bug Fixes` sections or `fix:` commits
12 * - **Performance** (⚡) — from `### Performance` sections or `perf:` commits
13 */
14
15 import { marked, type Token, type Tokens } from "marked";
16 import {
17 compareVersions,
18 getGitHubHeaders,
19 getGitHubReleasesUrl,
20 PRIMARY_UPGRADE_SOURCE,
21 type UpgradeSource,
22 } from ...
421 }
422
423 /** Options for source-aware changelog summary construction. */
424 type ChangelogBuildOptions = {
425 /** Maximum total list items across all sections, or unlimited when omitted. */
426 maxItems?: number;
427 /** Selected release source whose tag prefix filters the release list. */
428 source?: UpgradeSource;
429 };
430
431 function normalizeChangelogReleases(
432 releases: GitHubRelease[],
433 source: UpgradeSource
434 ): GitHubRelease[] {
435 if (isNormalizedForSource(releases, source)) {
436 return releases;
437 }
438 return [];
439 }
440
441 /** Build a changelog summary while filtering source-specific release tags. ...
1 /**
2 * Upgrade Module
3 *
4 * Detects how the CLI was installed and provides self-upgrade functionality.
5 * Binary management helpers (download URLs, locking, replacement) live in
6 * binary.ts and are shared with the setup --install flow.
7 */
8
9 import { spawn } from "node:child_process";
10 import {
11 chmodSync,
12 closeSync,
13 existsSync,
14 openSync,
15 realpathSync,
16 statSync,
17 unlinkSync,
18 writeSync,
19 } from "node:fs";
20 import { writeFile } from "node:fs/promises";
21 import { homedir } from "node:os";
22 import { dirname, isAbsolute, join, sep } from "node:path";
23 import { setTimeout } from "node:timers/promises";
24 import { prerelease as semverPrerelease, valid as semverValid } from "semver";
25 import {
26 acquireLock,
27 cleanupOldBinary,
28 compareVersions,
29 determineInstallDir,
30 ...
401 }
402
403 // Default to npm for other node_modules installations (npm, yarn classic)
404 return "npm";
405 }
406
407 /**
408 * Legacy detection for existing installs that don't have stored install info.
409 * Checks known curl install paths and package managers.
410 *
411 * @returns Detected installation method, or "unknown" if unable to determine
412 */
413 async function detectLegacyInstallationMethod(): Promise<InstallationMethod> {
414 // Check known curl install paths
415 for (const dir of getKnownCurlPaths()) {
416 if (process.execPath.startsWith(dir)) {
417 return "curl";
418 }
419 }
420
421 // Check package managers in order of popularity
422 const packageManagers: PackageManager[] = ["npm", "pnpm", "bun", "yarn"];
423
424 for (const pm of packageManagers) {
425 if (await isInstalledWith(pm)) {
426 return pm;
427 }
428 }
429
...
801 * @param version - Nightly version string (e.g., "0.14.0-dev.1772661724")
802 * @returns true if the nightly tag exists in GHCR, false if not found
803 * @throws {UpgradeError} On network failure or GHCR unavailability
804 */
805 async function nightlyVersionExists(
806 version: string,
807 source: UpgradeSource
808 ): Promise<boolean> {
809 const token = await getAnonymousToken(source);
810 try {
811 const manifest = await fetchManifest(
812 token,
813 `nightly-${version}`,
814 undefined,
815 source
816 );
817 validateNightlyManifestVersion(manifest, version);
818 return true;
819 } catch (error) {
820 if (error instanceof GhcrManifestHttpError && error.status === 404) {
821 return false;
822 }
823 throw error;
824 }
825 }
826
827 async function standaloneVersionExists(
828 version: string,
829 source?: ...
1201 throw new UpgradeError(
1202 "execution_failed",
1203 `Downloaded binary is missing or empty at ${path}. ` +
1204 "This is usually transient — rerun `sentry cli upgrade` to retry."
1205 );
1206 }
1207
1208 /**
1209 * Download the new binary to a temporary path and return its location.
1210 * Used by the upgrade command to download before spawning setup --install.
1211 *
1212 * For **nightly** versions (detected via {@link isNightlyVersion}), downloads
1213 * from GHCR using the OCI blob download protocol via {@link downloadNightlyToPath}.
1214 *
1215 * For **stable** versions, downloads from GitHub Releases via
1216 * {@link downloadStableToPath}.
1217 *
1218 * The lock is held on success so concurrent upgrades are blocked during the
1219 * download→spawn→install pipeline. ...
1 /**
2 * Background version check for "new version available" notifications.
3 *
4 * For nightly builds (CLI_VERSION contains "-dev.<timestamp>"), checks GHCR for the
5 * latest nightly version via the OCI manifest annotation. For stable builds,
6 * checks GitHub Releases. Results are cached in the database and shown on
7 * subsequent runs.
8 */
9
10 // biome-ignore lint/performance/noNamespaceImport: Sentry SDK recommends namespace import
11 import * as Sentry from "@sentry/node-core/light";
12 import { compare as semverCompare } from "semver";
13 import type { UpgradeSource } from "./binary.js";
14 import { CLI_VERSION } from "./constants.js";
15 import { getReleaseChannel } from "./db/release-channel.js";
16 import {
17 getVersionCheckInfo,
18 markUpdateNotified,
19 setVersionCheckInfo,
20 } from "./db/version-check.js";
21 import {
22 prefetchNightlyPatches,
23 ...
packages/cli/package.json packages/cli/src/lib/custom-ca.ts pnpm-lock.yaml
1 /**
2 * Custom CA certificate loading for corporate TLS proxies.
3 *
4 * Reads CA bundles from (in priority order):
5 * 1. `sentry cli defaults ca-cert` (stored path in SQLite)
6 * 2. `NODE_EXTRA_CA_CERTS` env var
7 *
8 * Returns a `tls` options object for Bun's `fetch()`. On the Node.js npm
9 * distribution, Node natively honors `NODE_EXTRA_CA_CERTS` so the extra
10 * `tls.ca` option is harmless (ignored by Node's fetch).
11 *
12 * Security model: When the CA source is an env var (not a stored default)
13 * AND the target is SaaS (`*.sentry.io`), a one-time warning is logged.
14 * `sentry cli defaults ca-cert` silences the warning — the user has
15 * explicitly acknowledged the custom CA. See CLI-1K6 plan for the full
16 * threat model discussion. ...
9525f72c70fabbb23ec21d13cf86add58f6287cc:packages/cli/src/commands/cli/upgrade.ts:53: fetchLatestVersion,
9525f72c70fabbb23ec21d13cf86add58f6287cc:packages/cli/src/commands/cli/upgrade.ts:59: resolveExistingUpgradeVersion,
9525f72c70fabbb23ec21d13cf86add58f6287cc:packages/cli/src/commands/cli/upgrade.ts:60: resolveLatestUpgradeVersion,
9525f72c70fabbb23ec21d13cf86add58f6287cc:packages/cli/src/commands/cli/upgrade.ts:62: versionExists,
9525f72c70fabbb23ec21d13cf86add58f6287cc:packages/cli/src/commands/cli/upgrade.ts:140: * fallback when `fetchLatestVersion()` hits a network error.
9525f72c70fabbb23ec21d13cf86add58f6287cc:packages/cli/src/commands/cli/upgrade.ts:301: if (!(await versionExists(lookupMethod, target))) {
9525f72c70fabbb23ec21d13cf86add58f6287cc:packages/cli/src/commands/cli/upgrade.ts:309: const resolved = await resolveExistingUpgradeVersion(target);
9525f72c70fabbb23ec21d13cf86add58f6287cc:packages/cli/src/commands/cli/upgrade.ts:341: ? ...
9525f72c70fabbb23ec21d13cf86add58f6287cc:packages/cli/src/commands/cli/upgrade.ts:47: fetchChangelog,
9525f72c70fabbb23ec21d13cf86add58f6287cc:packages/cli/src/commands/cli/upgrade.ts:52: executeUpgrade,
9525f72c70fabbb23ec21d13cf86add58f6287cc:packages/cli/src/commands/cli/upgrade.ts:514: // CLI-1D3). `downloadBinaryToTemp`'s visibility-race retry loop
9525f72c70fabbb23ec21d13cf86add58f6287cc:packages/cli/src/commands/cli/upgrade.ts:695: executeUpgrade(method, target, downloadTag, offline, setMessage, source)
9525f72c70fabbb23ec21d13cf86add58f6287cc:packages/cli/src/commands/cli/upgrade.ts:783: executeUpgrade("curl", target, downloadTag, undefined, setMessage, source)
9525f72c70fabbb23ec21d13cf86add58f6287cc:packages/cli/src/commands/cli/upgrade.ts:892: return fetchChangelog({
9525f72c70fabbb23ec21d13cf86add58f6287cc:packages/cli/src/lib/delta-upgrade.ts:531: // Expose `current` so attemptDeltaUpgrade's catch path can ...
9525f72c70fabbb23ec21d13cf86add58f6287cc:packages/cli/src/lib/delta-upgrade.ts:387: (await client.listTags(opts.token, PATCH_TAG_PREFIX, opts.signal));
9525f72c70fabbb23ec21d13cf86add58f6287cc:packages/cli/src/lib/delta-upgrade.ts:401: chainTags.map((tag) => client.fetchManifest(opts.token, tag, opts.signal))
9525f72c70fabbb23ec21d13cf86add58f6287cc:packages/cli/src/lib/ghcr.ts:258:export async function getAnonymousToken(
9525f72c70fabbb23ec21d13cf86add58f6287cc:packages/cli/src/lib/ghcr.ts:305: * @param token - Anonymous bearer token from {@link getAnonymousToken}
9525f72c70fabbb23ec21d13cf86add58f6287cc:packages/cli/src/lib/ghcr.ts:310:export async function fetchManifest(
9525f72c70fabbb23ec21d13cf86add58f6287cc:packages/cli/src/lib/ghcr.ts:351: * Convenience wrapper around {@link fetchManifest} for the rolling nightly tag.
9525f72c70fabbb23ec21d13cf86add58f6287cc:packages/cli/src/lib/ghcr.ts:353: * @param token - Anonymous bearer token from {@link ...
1 {
2 "name": "sentry",
3 "version": "0.45.0-dev.0",
4 "description": "Sentry CLI - A command-line interface for using Sentry built by robots and humans for robots and humans",
5 "repository": {
6 "type": "git",
7 "url": "git+https://github.com/getsentry/cli.git"
8 },
9 "main": "./dist/index.cjs",
10 "type": "module",
11 "types": "./dist/index.d.cts",
12 "exports": {
13 ".": {
14 "import": {
15 "types": "./dist/index.d.mts",
16 "default": "./dist/index.mjs"
17 },
18 "require": {
19 "types": "./dist/index.d.cts",
20 "default": "./dist/index.cjs"
21 }
22 }
23 },
24 "bin": {
25 "sentry": "./dist/bin.cjs"
26 },
27 "files": [
28 "dist/bin.cjs",
29 "dist/index.cjs",
30 "dist/index.mjs",
31 "dist/index.d.cts",
32 "dist/index.d.mts",
...
/usr/bin/bash: -c: line 1: unexpected EOF while looking for matching `"'
TAG PREFIX REFERENCES 9525f72c70fabbb23ec21d13cf86add58f6287cc:packages/cli/src/commands/issue/resolve.ts:113: " sentry issue resolve CLI-XX -i @commit:getsentry/cli@abc123\n" + 9525f72c70fabbb23ec21d13cf86add58f6287cc:packages/cli/src/commands/release/parse.ts:26: * @param arg - The raw positional argument (e.g., "my-org/1.0.0" or "sentry-cli@1.0.0") 9525f72c70fabbb23ec21d13cf86add58f6287cc:packages/cli/src/commands/release/view.ts:168: ' sentry release view "sentry-cli@0.24.0"\n' + 9525f72c70fabbb23ec21d13cf86add58f6287cc:packages/cli/src/lib/api/releases.ts:146: * @param version - Release version string (e.g., "1.0.0", "sentry-cli@0.24.0") 9525f72c70fabbb23ec21d13cf86add58f6287cc:packages/cli/src/lib/binary.ts:116: readonly tagPrefix: string; 9525f72c70fabbb23ec21d13cf86add58f6287cc:packages/cli/src/lib/binary.ts:124: tagPrefix: "cli@", 9525f72c70fabbb23ec21d13cf86add58f6287cc:packages/cli/src/lib/binary.ts:129: tagPrefix: ...
GHCR INVALID METADATA TESTS
9525f72c70fabbb23ec21d13cf86add58f6287cc:packages/cli/test/lib/delta-upgrade.test.ts:80: annotations[`sha256-${key}`] = value;
9525f72c70fabbb23ec21d13cf86add58f6287cc:packages/cli/test/lib/delta-upgrade.test.ts:1248: return new Response(JSON.stringify({ tags: [] }), { status: 200 });
9525f72c70fabbb23ec21d13cf86add58f6287cc:packages/cli/test/lib/ghcr.test.ts:535: { schemaVersion: 2, layers: [], annotations: ["value"] },
9525f72c70fabbb23ec21d13cf86add58f6287cc:packages/cli/test/lib/ghcr.test.ts:543: annotations: ["value"],
9525f72c70fabbb23ec21d13cf86add58f6287cc:packages/cli/test/lib/ghcr.test.ts:547: ])("rejects invalid OCI manifest %#", async (manifest) => {
9525f72c70fabbb23ec21d13cf86add58f6287cc:packages/cli/test/lib/ghcr.test.ts:551: 'Manifest for tag "nightly" returned invalid metadata'
9525f72c70fabbb23ec21d13cf86add58f6287cc:packages/cli/test/lib/ghcr.test.ts:617: test("rejects a repeated pagination cursor", async () => ...
9525f72c7 fix(cli): harden upgrade metadata validation
packages/cli/src/lib/ghcr.ts | 15 +++-
packages/cli/src/lib/upgrade.ts | 43 ++++++++--
packages/cli/test/commands/cli/upgrade.test.ts | 101 ++++++++++++++++++++++-
packages/cli/test/lib/ghcr.test.ts | 33 ++++++++
packages/cli/test/lib/upgrade.test.ts | 106 +++++++++++++++++++++----
5 files changed, 270 insertions(+), 28 deletions(-)
diff --git a/packages/cli/src/lib/ghcr.ts b/packages/cli/src/lib/ghcr.ts
index c7b8c37b2..5bca7c061 100644
--- a/packages/cli/src/lib/ghcr.ts
+++ b/packages/cli/src/lib/ghcr.ts
@@ -209,6 +209,7 @@ function isStringRecord(value: unknown): value is Record<string, string> {
return (
typeof value === "object" &&
value !== null &&
+ !Array.isArray(value) &&
Object.values(value).every((item) => typeof item === "string")
);
}
@@ -286,7 +287,8 @@ export async function getAnonymousToken(
data === null ||
!("token" ...
9525f72c70fabbb23ec21d13cf86add58f6287cc:.github/workflows/sentry-release.yml:38: VERSION: ${{ github.event.release.tag_name || inputs.version }}
9525f72c70fabbb23ec21d13cf86add58f6287cc:.github/workflows/sentry-release.yml:54: run: npm install -g "sentry@${VERSION}"
9525f72c70fabbb23ec21d13cf86add58f6287cc:.github/workflows/sentry-release.yml:74: --url "https://github.com/${{ github.repository }}/releases/tag/${VERSION}"
9525f72c70fabbb23ec21d13cf86add58f6287cc:packages/cli/script/bundle.ts:384:// with the npm package so `npx sentry@latest init` can load the
9525f72c70fabbb23ec21d13cf86add58f6287cc:packages/cli/test/commands/cli/upgrade.test.ts:413: test("uses the cached target only after a transport failure", async () => {
9525f72c70fabbb23ec21d13cf86add58f6287cc:packages/cli/test/commands/cli/upgrade.test.ts:429: expect(getOutput()).toContain("Using cached target: 88.88.88");
9525f72c70fabbb23ec21d13cf86add58f6287cc:packages/cli/test/commands/cli/upgrade.test.ts:432: test("uses the cached target after response body transport failure", async () => {
9525f72c70fabbb23ec21d13cf86add58f6287cc:packages/cli/test/commands/cli/upgrade.test.ts:452: expect(getOutput()).toContain("Using cached target: 88.88.88");
9525f72c70fabbb23ec21d13cf86add58f6287cc:packages/cli/test/commands/cli/upgrade.test.ts:461: ])("never uses the cached target after %s", async (_name, response) => {
9525f72c70fabbb23ec21d13cf86add58f6287cc:packages/cli/test/commands/cli/upgrade.test.ts:479: expect(getOutput()).not.toContain("Using cached ...
Your replacement security review returned EMPTY. Resume the SAME exact-head immutable review now. A non-empty response is mandatory. If any repository/tool access fails, report BLOCKED first with the exact command/error and end DO-NOT-MERGE. Otherwise provide substantive findings-first evidence for exact base ec83887a16f780f32fba4b7d710bad262dba3a22 and head 9525f72c70fabbb23ec21d13cf86add58f6287cc. State immutable base/head trees, merge base, changed paths inspected, patch hash or equivalent immutable verification, and git diff --check result. Re-evaluate every security contract from the prior prompt with exact HEAD file:line citations: typed 404-only fallback; terminal non-404/transport/malformed responses; source affinity; trusted pagination; body cancellation and transport classification; npm shape/status and no-spawn package-manager failures; canonical Toolkit/legacy tags; pinned metadata; strict GHCR token/OCI/tag validation and cursor cycle; auth-free redirects; channel/offline/cache/final-SHA behavior. Source-neutral patch caching remains settled under byte-identical dual publication and final SHA verification; raise it only with a demonstrated surviving defect. Report PASS/CONCERN/MUST-FIX/BLOCKED and residual risks even with no defects. Never inspect or modify the worktree or mutate VCS. End exactly MERGE or DO-NOT-MERGE. Empty output is forbidden.
DELTA FAILURE/CLEANUP ASSERTIONS
9525f72c70fabbb23ec21d13cf86add58f6287cc:packages/cli/test/lib/delta-upgrade.mocked.test.ts:14:import { copyFileSync, existsSync, unlinkSync, writeFileSync } from "node:fs";
9525f72c70fabbb23ec21d13cf86add58f6287cc:packages/cli/test/lib/delta-upgrade.mocked.test.ts:167: ).rejects.toThrow("SHA-256 mismatch");
9525f72c70fabbb23ec21d13cf86add58f6287cc:packages/cli/test/lib/delta-upgrade.mocked.test.ts:169: if (existsSync(oldBinaryPath)) unlinkSync(oldBinaryPath);
9525f72c70fabbb23ec21d13cf86add58f6287cc:packages/cli/test/lib/delta-upgrade.mocked.test.ts:170: if (existsSync(destPath)) unlinkSync(destPath);
9525f72c70fabbb23ec21d13cf86add58f6287cc:packages/cli/test/lib/delta-upgrade.mocked.test.ts:189: if (existsSync(oldBinaryPath)) unlinkSync(oldBinaryPath);
9525f72c70fabbb23ec21d13cf86add58f6287cc:packages/cli/test/lib/delta-upgrade.mocked.test.ts:251: if (existsSync(oldBinaryPath)) ...
DIFF_CHECK_EXIT=0
OBJECTS ec83887a16f780f32fba4b7d710bad262dba3a22 110d6530e57ca4fbfb2a3f98e52013d2668b5b5e 9525f72c70fabbb23ec21d13cf86add58f6287cc 22f49363a13e32c36dcd9e1ca95916809c2dc271 MERGE_BASE ec83887a16f780f32fba4b7d710bad262dba3a22 CHANGED_PATHS 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/errors.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 PATCH_SHA256 4950719273dc17d5a9e061b8cfe70c62131fe3299dc1799d9c09bb522b...
INSPECTED_HEAD_BLOBS_OK=17
1 /**
2 * Binary Management
3 *
4 * Shared utilities for installing, replacing, and managing the CLI binary.
5 * Used by both `setup --install` (fresh installs) and `upgrade` (self-updates).
6 */
7
8 import { spawnSync } from "node:child_process";
9 import {
10 existsSync,
11 mkdirSync,
12 readFileSync,
13 renameSync,
14 unlinkSync,
15 writeFileSync,
16 } from "node:fs";
17 import { chmod, copyFile, mkdir, realpath, unlink } from "node:fs/promises";
18 import { delimiter, dirname, isAbsolute, join, resolve, sep } from "node:path";
19 import { compare as semverCompare } from "semver";
20 import { getUserAgent } from "./constants.js";
21 import {
22 buildTlsErrorDetail,
23 customFetch,
24 isTlsCertError,
25 } from "./custom-ca.js";
26 import {
27 stringifyUnknown,
28 UpgradeError,
29 UpgradeTransportError,
30 } from ...
diff --git a/packages/cli/src/lib/binary.ts b/packages/cli/src/lib/binary.ts
index 75582612a..e64f5059e 100644
--- a/packages/cli/src/lib/binary.ts
+++ b/packages/cli/src/lib/binary.ts
@@ -1,470 +1,641 @@
/**
* Binary Management
*
* Shared utilities for installing, replacing, and managing the CLI binary.
* 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 ...
250 throw new UpgradeError("unknown_method");
251 }
252 // Homebrew manages versioning through the formula — pinning a specific
253 // stable version is not supported via this command.
254 const pinnedVersion = CHANNEL_VERSIONS.has(versionArg ?? "")
255 ? undefined
256 : versionArg?.replace(VERSION_PREFIX_REGEX, "");
257 if (method === "brew" && pinnedVersion && !isNightlyVersion(pinnedVersion)) {
258 throw new UpgradeError(
259 "unsupported_operation",
260 "Homebrew does not support installing a specific version. Run 'brew upgrade getsentry/tools/sentry' to upgrade to the latest formula version."
261 );
262 }
263 // Offline mode is only supported for curl-installed binaries — package
264 // managers always need network to fetch and install packages.
265 if (offline && method !== "curl") {
266 throw new UpgradeError(
267 "unsupported_operation",
268 "Offline ...
920 }
921
922 export const upgradeCommand = buildCommand({
923 auth: false,
924 docs: {
925 brief: "Update the Sentry CLI to the latest version",
926 fullDescription:
927 "Check for updates and upgrade the Sentry CLI to the latest or a specific version.\n\n" +
928 "By default, detects how the CLI was installed (npm, curl, etc.) and uses the same method to upgrade.\n\n" +
929 "Two release channels are supported:\n" +
930 " stable (default) Latest stable release\n" +
931 " nightly Built from main, updated on every commit\n\n" +
932 "The channel is persisted so that subsequent bare `sentry cli upgrade` calls\n" +
933 "use the same channel.\n\n" +
934 "Examples:\n" +
935 " sentry cli upgrade # Update to latest (using persisted channel)\n" +
936 " sentry cli upgrade nightly # Switch to nightly channel and update\n" +
937 " ...
9525f72c70fabbb23ec21d13cf86add58f6287cc:packages/cli/src/commands/cli/upgrade.ts:39:import { UpgradeError, UpgradeTransportError } from "../../lib/errors.js";
9525f72c70fabbb23ec21d13cf86add58f6287cc:packages/cli/src/commands/cli/upgrade.ts:146:function resolveOfflineTarget(versionArg: string | undefined): string {
9525f72c70fabbb23ec21d13cf86add58f6287cc:packages/cli/src/commands/cli/upgrade.ts:169:async function resolveTargetWithFallback(opts: {
9525f72c70fabbb23ec21d13cf86add58f6287cc:packages/cli/src/commands/cli/upgrade.ts:201: const target = resolveOfflineTarget(versionArg);
9525f72c70fabbb23ec21d13cf86add58f6287cc:packages/cli/src/commands/cli/upgrade.ts:225: if (method !== "curl" || !(error instanceof UpgradeTransportError)) {
9525f72c70fabbb23ec21d13cf86add58f6287cc:packages/cli/src/commands/cli/upgrade.ts:229: const target = resolveOfflineTarget(versionArg);
9525f72c70fabbb23ec21d13cf86add58f6287cc:packages/cli/src/commands/cli/upgrade.ts:862: * cache on channel ...
120 } {
121 // "nightly" and "stable" as positional args select the channel rather than
122 // installing a specific version. Match case-insensitively for convenience.
123 const lower = positional?.toLowerCase();
124 if (lower === "nightly" || lower === "stable") {
125 return {
126 channel: lower,
127 versionArg: undefined,
128 };
129 }
130
131 return {
132 channel: getReleaseChannel(),
133 versionArg: positional,
134 };
135 }
136
137 /**
138 * Resolve the target version from the local cache (SQLite) instead of
139 * fetching from the network. Used by `--offline` and as automatic
140 * fallback when `fetchLatestVersion()` hits a network error. ...
1 /**
2 * GHCR (GitHub Container Registry) Client
3 *
4 * Encapsulates the OCI download protocol for fetching nightly CLI binaries
5 * from ghcr.io/getsentry/cli. Nightly builds are pushed as OCI artifacts
6 * via ORAS with the version baked into the manifest annotation.
7 *
8 * Key design decisions:
9 * - Anonymous access: nightly package is public; no token needed beyond the
10 * standard ghcr.io anonymous token exchange.
11 * - Version discovery from manifest annotation: `annotations.version` in the
12 * OCI manifest holds the nightly version. Checking the latest version only
13 * requires a token exchange + manifest fetch (2 HTTP requests total).
14 * - Redirect quirk: ghcr.io blob downloads return 307 to Azure Blob Storage.
15 * Using `fetch` with `redirect: "follow"` would forward the Authorization
16 * header to Azure, which returns 404. ...
Date: Sep 10, 2026 * 🔴 (10:51) User assigned the assistant as the replacement READ-ONLY security and supply-chain merge-gate reviewer for getsentry/cli PR #1569; prior reviewer returned empty twice, and a substantive evidence-based response is mandatory. * 🔴 (10:51) User specified repository `/home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/toolkit-bridge-upgrad…