Dashboard › cli › Session 1QLmcLAwo3gA
1QLmcLAwo3gAPitU6Independent READ-ONLY security merge gate for getsentry/cli PR #1569. IMPORTANT: review ONLY these exact revisions, copied verbatim: BASE=ec83887a16f780f32fba4b7d710bad262dba3a22 HEAD=9525f72c70fabbb23ec21d13cf86add58f6287cc. Repository path: /home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/toolkit-bridge-upgrade. Your FIRST command must be exactly equivalent to git -C <repo> cat-file -e ec83887a16f780f32fba4b7d710bad262dba3a22^{commit} && git -C <repo> cat-file -e 9525f72c70fabbb23ec21d13cf86add58f6287cc^{commit}. Never substitute, derive, or use any other revisions. Use immutable git show/git diff evidence only; never edit files or mutate VCS. Inspect all changed source/test objects and verify merge base, base/head trees, changed paths, full diff, diff-check, and patch SHA-256. Find security defects in 404-only fallback, source affinity, trusted pagination, abort/body classification, npm/GHCR schemas and status handling, no-spawn validation failures, pin/tag validation, redirect credential isolation, channel/offline behavior, and delta integrity. Accepted contract: patch cache is source-neutral because dual-published artifacts are byte-identical and final output SHA-256 is verified. Return a substantive NON-EMPTY findings-first report with exact HEAD file:line evidence, classifications MUST-FIX/CONCERN/PASS, deterministic regression and fix for each defect, scope and residual risks. If the exact initial command fails, report BLOCKED with its exact output. Final line exactly MERGE or DO-NOT-MERGE.
(no output)
ec83887a16f780f32fba4b7d710bad262dba3a22
ec83887a16f780f32fba4b7d710bad262dba3a22 110d6530e57ca4fbfb2a3f98e52013d2668b5b5e 9525f72c70fabbb23ec21d13cf86add58f6287cc 22f49363a13e32c36dcd9e1ca95916809c2dc271
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 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 | ...
(no output)
4950719273dc17d5a9e061b8cfe70c62131fe3299dc1799d9c09bb522b112193 -
...output truncated...
Full output saved to: /home/byk/.local/share/opencode/tool-output/tool_08b30d60c001JPWD8Mv5U1fhwv
+ ).rejects.toMatchObject({ name: "AbortError" });
+ expect(requestCount).toBe(1);
+ });
+
+ test("preserves an arbitrary external cancellation reason", async () => {
+ const controller = new AbortController();
+ const reason = new Error("cancelled");
+ let requestCount = 0;
+ mockFetch(async () => {
+ requestCount += 1;
+ controller.abort(reason);
+ throw new TypeError("invalid_argument");
+ });
+
+ await expect(
+ downloadNightlyBlob("token", "sha256:abc", controller.signal)
+ ).rejects.toBe(reason);
+ expect(requestCount).toBe(1);
+ });
+
+ test("preserves external cancellation during the redirect request", async () => {
+ const controller = new AbortController();
+ const reason = { kind: "cancelled" };
+ const headers: Headers[] = [];
+ mockFetch(async (_url, init) => {
+ headers.push(new ...
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 @@ -1,96 +1,100 @@ /** * 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/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,154 +1,185 @@
/**
* 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 ...
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
@@ -1,338 +1,431 @@
/** 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/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
@@ -541,160 +541,168 @@ export function validationError(
headline: string,
examples: string[],
field?: string,
note?: string
): ValidationError {
return new ValidationError(
buildValidationMessage(headline, examples, note),
field
);
}
/**
* Input validation errors.
*
* @param message - Validation failure description
* @param field - Name of the invalid field
*/
export class ValidationError extends CliError {
readonly field?: string;
constructor(message: string, field?: string) {
super(message, EXIT.VALIDATION);
this.name = "ValidationError";
this.field = field;
}
}
/**
* OAuth device flow errors (RFC 8628).
*
* @param code - OAuth error code (e.g., "authorization_pending", "slow_down")
* @param description - Human-readable ...
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 @@ -1,520 +1,659 @@ /** * 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/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 @@ -1,72 +1,79 @@ /** * 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/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
@@ -1,119 +1,217 @@
/**
* 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,
...
packages/cli/src/commands/cli/upgrade.ts 1114 packages/cli/src/lib/binary.ts 768 packages/cli/src/lib/delta-upgrade.ts 725 packages/cli/src/lib/errors.ts 1012 packages/cli/src/lib/ghcr.ts 659 packages/cli/src/lib/release-notes.ts 793 packages/cli/src/lib/upgrade.ts 1502 packages/cli/src/lib/version-check.ts 501
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 ...
497 export async function fetchLatestFromGitHubWithSource(
498 signal?: AbortSignal,
499 sources: readonly UpgradeSource[] = UPGRADE_SOURCES
500 ): Promise<ResolvedUpgradeVersion> {
501 const resolved = await resolveUpgradeSource({
502 getProbeUrl: getGitHubLatestReleaseUrl,
503 signal,
504 sources,
505 });
506 let response = resolved.response;
507 const visitedPages = new Set([getGitHubLatestReleaseUrl(resolved.source)]);
508 const versions: string[] = [];
509 while (true) {
510 const data = await parseUpgradeJson(
511 response,
512 signal,
513 "GitHub returned invalid release metadata"
514 );
515 versions.push(...extractReleaseVersions(data, resolved.source));
516 const nextPage = getNextGitHubReleasePage(response, resolved.source);
517 if (!nextPage) {
518 const version = versions.sort((a, b) => compareVersions(b, a))[0];
519 if ...
932 /**
933 * Drain a decompressed body into `fd`, awaiting the underlying stream
934 * pipeline. Returns the terminal stream error (if any) and any error
935 * raised by the synchronous write loop. The fd is owned by the caller —
936 * this function never closes it.
937 */
938 async function drainBodyToFd(
939 body: ReadableStream<Uint8Array>,
940 fd: number,
941 onBytes: (n: number) => void
942 ): Promise<{ streamError: unknown; writeError: Error | undefined }> {
943 let writeError: Error | undefined;
944 let streamError: unknown;
945 try {
946 for await (const chunk of body.pipeThrough(
947 new DecompressionStream("gzip")
948 )) {
949 if (writeError) {
950 break;
951 }
952 try {
953 writeChunkSync(fd, chunk);
954 onBytes(chunk.byteLength);
955 } catch (err) {
956 writeError = err instanceof Error ? ...
9525f72c70fabbb23ec21d13cf86add58f6287cc:packages/cli/test/commands/cli/upgrade.test.ts:832:// Download + setup paths (Option B: child_process.spawn spy)
9525f72c70fabbb23ec21d13cf86add58f6287cc:packages/cli/test/commands/cli/upgrade.test.ts:836:// 2. Spying on child_process.spawn so it resolves immediately with exit 0
9525f72c70fabbb23ec21d13cf86add58f6287cc:packages/cli/test/commands/cli/upgrade.test.ts:838:// child_process.spawn is spied via spyOn so the module-level import in the
9525f72c70fabbb23ec21d13cf86add58f6287cc:packages/cli/test/commands/cli/upgrade.test.ts:844: * exit code on the next microtask. Used to mock child_process.spawn in tests.
9525f72c70fabbb23ec21d13cf86add58f6287cc:packages/cli/test/commands/cli/upgrade.test.ts:854:describe("sentry cli upgrade — curl full upgrade path (child_process.spawn spy)", () => {
9525f72c70fabbb23ec21d13cf86add58f6287cc:packages/cli/test/commands/cli/upgrade.test.ts:855: ...
760 layers: [],
761 annotations: { version: "0.0.0-dev.1740393600" },
762 }),
763 { status: 200 }
764 );
765 }
766 return new Response("Not Found", { status: 404 });
767 });
768
769 const version = await fetchLatestVersion("npm", "nightly");
770 expect(version).toBe("0.0.0-dev.1740393600");
771 });
772
773 test("defaults to stable channel (uses GitHub) when channel omitted", async () => {
774 mockFetch(
775 async () =>
776 new Response(JSON.stringify([{ tag_name: "cli@3.0.0" }]), {
777 status: 200,
778 headers: { "Content-Type": "application/json" },
779 })
780 );
781
782 const version = await fetchLatestVersion("curl");
783 expect(version).toBe("3.0.0");
784 });
785 });
786
787 describe("versionExists", () => {
788 test.each([
789 401, 403, 429, ...
90 forced: boolean;
91 /** Whether the upgrade was performed offline (from cache) */
92 offline?: boolean;
93 /** Warnings to display (e.g., PATH shadowing from old package manager install) */
94 warnings?: string[];
95 /** Changelog summary for the version range. Absent for offline or on fetch failure. */
96 changelog?: ChangelogSummary;
97 };
98
99 type UpgradeFlags = {
100 readonly check: boolean;
101 readonly force: boolean;
102 readonly offline: boolean;
103 readonly "no-agent-skills": boolean;
104 readonly method?: InstallationMethod;
105 /** Injected by buildCommand output wrapper — suppresses spinners */
106 readonly json?: boolean;
107 };
108
109 /**
110 * Resolve effective channel and version arg from the positional `version`
111 * parameter. "nightly" and "stable" are treated as channel selectors, not
112 * literal version strings. ...
9525f72c70fabbb23ec21d13cf86add58f6287cc:packages/cli/src/lib/delta-upgrade.ts:134: return `patch-chain:${fromVersion}-${toVersion}`;
9525f72c70fabbb23ec21d13cf86add58f6287cc:packages/cli/src/lib/formatters/human.ts:2339: `${colorTag("green", "✓")} ${verb} to ${safeCodeSpan(data.targetVersion)}${escapeMarkdownInline(" (offline, from cache)")}`
9525f72c70fabbb23ec21d13cf86add58f6287cc:packages/cli/test/lib/bspatch.test.ts:279: `bspatch-chain-${Date.now()}-${Math.random().toString(36).slice(2)}-${name}`
9525f72c70fabbb23ec21d13cf86add58f6287cc:packages/cli/test/lib/db/dsn-cache.model-based.test.ts:153:class SetCachedDsnCommand implements AsyncCommand<CacheModel, RealCache> {
9525f72c70fabbb23ec21d13cf86add58f6287cc:packages/cli/test/lib/db/dsn-cache.model-based.test.ts:187:class GetCachedDsnCommand implements AsyncCommand<CacheModel, RealCache> {
9525f72c70fabbb23ec21d13cf86add58f6287cc:packages/cli/test/lib/db/dsn-cache.model-based.test.ts:218:class ...
90 }
91
92 /**
93 * How the CLI was installed. Determines the upgrade strategy.
94 *
95 * Defined here (alongside other installation constants like
96 * {@link KNOWN_CURL_DIRS}) so that both `upgrade.ts` and
97 * `db/install-info.ts` can import it without creating a circular
98 * dependency.
99 */
100 export type InstallationMethod =
101 | "curl"
102 | "brew"
103 | "npm"
104 | "pnpm"
105 | "bun"
106 | "yarn"
107 | "unknown";
108
109 /** A repository pair that hosts CLI stable releases and nightly OCI images. */
110 export type UpgradeSource = {
111 /** GitHub `owner/repository` containing CLI release assets. */
112 readonly githubRepo: string;
113 /** GHCR `owner/package` containing CLI nightly images and delta patches. */
114 readonly ghcrRepo: string;
115 /** Prefix attached to CLI release tags in this repository. ...
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. ...
9525f72c70fabbb23ec21d13cf86add58f6287cc:packages/cli/src/commands/cli/upgrade.ts:309: const resolved = await resolveExistingUpgradeVersion(target);
9525f72c70fabbb23ec21d13cf86add58f6287cc:packages/cli/src/commands/cli/upgrade.ts:341: ? await resolveLatestUpgradeVersion(channel)
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/lib/upgrade.ts:497:export async function fetchLatestFromGitHubWithSource(
9525f72c70fabbb23ec21d13cf86add58f6287cc:packages/cli/src/lib/upgrade.ts:554: await fetchLatestFromGitHubWithSource(
9525f72c70fabbb23ec21d13cf86add58f6287cc:packages/cli/src/lib/upgrade.ts:629:export async function ...
900
901 // Clean up any temp binary files written to the redirected install path
902 for (const suffix of ["", ".download", ".old", ".lock"]) {
903 try {
904 await unlink(join(spawnBinDir, `${binName}${suffix}`));
905 } catch {
906 // Ignore
907 }
908 }
909 clearInstallInfo();
910 });
911
912 /**
913 * Mock fetch to serve both the GitHub latest-release version endpoint and a
914 * minimal valid gzipped binary for downloadBinaryToTemp.
915 */
916 function mockBinaryDownloadWithVersion(version: string): void {
917 const fakeContent = new Uint8Array([0x7f, 0x45, 0x4c, 0x46]); // ELF magic
918 const gzipped = gzipSync(fakeContent);
919 mockFetch(async (url) => {
920 const urlStr = String(url);
921 if (urlStr.includes("getsentry/toolkit/releases?per_page=100")) {
922 return new Response(JSON.stringify([{ tag_name: ...
9525f72c70fabbb23ec21d13cf86add58f6287cc:packages/cli/src/lib/custom-ca.ts:305:export function customFetch(
260 const cause = getTlsCertErrorMessage(error) ?? error.message;
261 const hasCustomCa = getCustomCaSource() !== "none";
262
263 if (hasCustomCa) {
264 return (
265 `TLS certificate verification failed: ${cause}\n\n` +
266 " Custom CA certificates are loaded but verification still failed.\n" +
267 " The certificate file may not contain the correct CA for this server.\n\n" +
268 " Check that your CA bundle includes the certificate authority used by\n" +
269 " your network proxy or Sentry instance."
270 );
271 }
272
273 return (
274 `TLS certificate verification failed: ${cause}\n\n` +
275 " This usually means your network uses a TLS-intercepting proxy\n" +
276 " (corporate firewall, VPN) with a private certificate authority.\n\n" +
277 " To fix this, point the CLI to your CA certificate bundle:\n" +
278 " sentry cli defaults ca-cert ...
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
@@ -20,34 +20,39 @@ import { afterEach, beforeEach, describe, expect, test } from "vitest";
import {
acquireLock,
compareVersions,
determineInstallDir,
fetchWithUpgradeError,
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";
describe("getBinaryDownloadUrl", () => {
test("builds correct URL for current platform", () => {
const url = getBinaryDownloadUrl("1.0.0");
- ...
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
@@ -1,25 +1,27 @@
/**
* GHCR Client Tests
*
* Unit tests for the GHCR/OCI download protocol helpers.
* All HTTP calls are mocked via globalThis.fetch to avoid network access.
*/
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,
downloadNightlyBlob,
fetchManifest,
fetchNightlyManifest,
findLayerByFilename,
GHCR_REPO,
GHCR_TAG,
+ GhcrManifestHttpError,
getAnonymousToken,
getNightlyVersion,
listTags,
type OciManifest,
} from "../../src/lib/ghcr.js";
/** Store original fetch for restoration */
let originalFetch: typeof globalThis.fetch;
@@ -32,31 +34,31 @@ function ...
300 "Nightly manifest has invalid version annotation"
301 );
302 });
303 });
304
305 describe("findLayerByFilename", () => {
306 test("finds layer by filename annotation", () => {
307 const manifest = makeManifest();
308 const layer = findLayerByFilename(manifest, "sentry-linux-x64.gz");
309 expect(layer.digest).toBe(`sha256:${"a".repeat(64)}`);
310 });
311
312 test("finds darwin layer", () => {
313 const manifest = makeManifest();
314 const layer = findLayerByFilename(manifest, "sentry-darwin-arm64.gz");
315 expect(layer.digest).toBe(`sha256:${"d".repeat(64)}`);
316 });
317
318 test("throws UpgradeError when filename not found", () => {
319 const manifest = makeManifest();
320 expect(() =>
321 findLayerByFilename(manifest, "sentry-freebsd-x64.gz")
322 ).toThrow(UpgradeError);
323 expect(() =>
324 findLayerByFilename(manifest, ...
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 ...
9525f72c70fabbb23ec21d13cf86add58f6287cc:packages/cli/package.json:111: "binpatch": "^0.4.2",
9525f72c70fabbb23ec21d13cf86add58f6287cc:pnpm-lock.yaml:2696: binpatch@0.4.2:
9525f72c70fabbb23ec21d13cf86add58f6287cc:pnpm-lock.yaml:7801: binpatch@0.4.2: {}
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
@@ -94,20 +94,20 @@ describe("resolveStableDelta", () => {
// Create a "current binary" to patch from
const oldBinaryPath = tempFile("old-binary.bin");
const destPath = tempFile("patched-binary.bin");
writeFileSync(oldBinaryPath, Buffer.from("old binary content for testing"));
// 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 = ...
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
@@ -119,39 +119,39 @@ describe("upgradeCommand.func", () => {
// Note: We skip testing "unknown installation method" case because
// detectInstallationMethod() runs actual shell commands (npm list, etc.)
// which can be slow/flaky in CI. The unknown method handling is tested
// indirectly through the upgrade.ts unit tests in lib/upgrade.test.ts.
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;
const func = await upgradeCommand.loader();
const { ...
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
@@ -38,10 +38,11 @@ import {
} from "../../../src/lib/db/install-info.js";
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 */
let originalFetch: typeof globalThis.fetch;
@@ -170,10 +171,14 @@ function createMockContext(
*/
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 ...
1078
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
@@ -118,10 +118,11 @@ import { isEnoentSpawnError } from "../../src/commands/cli/upgrade.js";
import {
acquireLock,
getBinaryDownloadUrl,
isNightlyVersion,
releaseLock,
+ UPGRADE_SOURCES,
} from "../../src/lib/binary.js";
import {
clearInstallInfo,
setInstallInfo,
} from "../../src/lib/db/install-info.js";
@@ -138,10 +139,11 @@ const {
fetchLatestFromNpm,
fetchLatestNightlyVersion,
fetchLatestVersion,
getCurlInstallPaths,
parseInstallationMethod,
+ resolveExistingUpgradeVersion,
startCleanupOldBinary,
versionExists,
} = await import("../../src/lib/upgrade.js");
import { TEST_TMP_DIR, useTestConfigDir } from "../helpers.js";
@@ -186,58 +188,250 @@ describe("parseInstallationMethod", () => {
expect(() => ...
});
test("uses GitHub for brew method", async () => {
mockFetch(
async () =>
- new Response(JSON.stringify({ tag_name: "v2.0.0" }), {
+ new Response(JSON.stringify([{ tag_name: "cli@2.0.0" }]), {
status: 200,
headers: { "Content-Type": "application/json" },
})
);
@@ -488,16 +718,21 @@ describe("fetchLatestVersion", () => {
test("uses GHCR manifest when channel is nightly (curl method)", async () => {
// Nightly version is now fetched from GHCR manifest annotation, not version.json
mockFetch(async (url) => {
const urlStr = String(url);
+ if (urlStr === "https://api.github.com/repos/getsentry/toolkit") {
+ return new Response(null, { status: 200 });
+ }
if (urlStr.includes("ghcr.io/token")) {
return new Response(JSON.stringify({ token: "tok" }), { status: 200 });
}
if (urlStr.includes("/manifests/nightly")) {
return new Response(
...
await expect(
versionExists("curl", "0.14.0-dev.1772661724")
).rejects.toThrow(UpgradeError);
});
+ test("does not classify nightly transport error text as not found", async () => {
+ mockFetch(async (url) => {
+ if (String(url).includes("ghcr.io/token")) {
+ return new Response(JSON.stringify({ token: "tok" }), { status: 200 });
+ }
+ throw new Error("HTTP 404");
+ });
+
+ await expect(
+ versionExists("curl", "0.14.0-dev.1772661724", UPGRADE_SOURCES[0])
+ ).rejects.toThrow("HTTP 404");
+ });
+
test("throws on GHCR server error for nightly version", async () => {
mockFetch(async (url) => {
const u = String(url);
if (u.includes("ghcr.io/token")) {
return new Response(JSON.stringify({ token: "tok" }), { status: 200 });
@@ -700,10 +1205,28 @@ describe("versionExists", () => {
});
await expect(
versionExists("curl", "0.14.0-dev.1772661724")
).rejects.toThrow(UpgradeError);
...
390 */
391 function buildCheckResult(opts: {
392 target: string;
393 versionArg: string | undefined;
394 method: InstallationMethod;
395 channel: ReleaseChannel;
396 flags: UpgradeFlags;
397 }): UpgradeResult {
398 const { target, versionArg, method, channel, flags } = opts;
399 const result: UpgradeResult = {
400 action: "checked",
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 ...
836 /**
837 * Resolve the channel, version arg, method, and channel-changed flag from
838 * the positional version argument and flags. Extracted to keep `func()`
839 * complexity under the biome limit.
840 */
841 async function resolveContext(
842 version: string | undefined,
843 flags: UpgradeFlags
844 ): Promise<{
845 channel: ReleaseChannel;
846 versionArg: string | undefined;
847 channelChanged: boolean;
848 method: InstallationMethod;
849 }> {
850 const { channel, versionArg } = resolveChannelAndVersion(version);
851 const currentChannel = getReleaseChannel();
852 const channelChanged = channel !== currentChannel;
853
854 const method = flags.method ?? (await detectInstallationMethod());
855 validateMethod(method, versionArg, flags.offline);
856 return { channel, versionArg, channelChanged, method };
857 }
858
859 /**
860 * Persist the release channel preference. ...
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
@@ -4,26 +4,32 @@
* 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 "../../src/lib/delta-upgrade.js";
import {
buildChangelogSummary,
...
Your review returned EMPTY. Resume this SAME review of BASE ec83887a16f780f32fba4b7d710bad262dba3a22 and HEAD 9525f72c70fabbb23ec21d13cf86add58f6287cc. Return non-empty text now. If blocked, quote the exact failing command/error and end DO-NOT-MERGE. Otherwise list security findings with exact HEAD file:line evidence, scope, immutable tree/diff verification, and residual risks; classify MUST-FIX/CONCERN/PASS. End exactly MERGE or DO-NOT-MERGE. Do not edit anything.
PASS — No MUST-FIX or CONCERN security defect found. No defect requires a regression or fix.
binary.ts:344-360 falls through only on HTTP 404. Nightly resolution uses typed 404 errors at upgrade.ts:643-672 and ghcr.ts:154-166; transport text cannot trigger fallback.commands/cli/upgrade.ts:328-348, 687-695, 1017-1037, 1072-1097; delta-upgrade.ts:176-225, 618-724).api.github.com, an approved path, and a positive page number, then rebuilds the URL from the selected source (upgrade.ts:149-185). Cycles fail closed at upgrade.ts:527-544.ghcr.ts:107-140, 258-345, 432-510).upgrade.ts:567-615, 871-896). GHCR validates token metadata, OCI schema, SHA-256 digests, sizes, annotations, and nightly versions (ghcr.ts:206-299, 310-389). Only 404 means missing.commands/cli/upgrade.ts:296-385, 1055-1097; upgrade.ts:735-897). Command regressions assert no spawn for missing versions, non-404 failures, and malformed npm metadata at test/commands/cli/upgrade.test.ts:1022-1095.upgrade.ts:735-790). Nightly annotations require numeric X.Y.Z-dev.N SemVer and must equal the requested version (ghcr.ts:374-389; upgrade.ts:722-733). ...* 🔴 (12:00) User stated package managers always need network access to fetch and install packages; offline upgrades are therefore supported only for curl-installed binaries, with other methods throwing `UpgradeError("unsupported_operation", "Offline upgrade is only supported for curl-installed binaries.")`. * 🔴 (12:00) User stated nightly builds use curl/GitHub lookup regardless of the current …
* 🟡 (11:58) Exact BASE/HEAD commit-object verification completed successfully with no output for BASE=`ec83887a16f780f32fba4b7d710bad262dba3a22` and HEAD=`9525f72c70fabbb23ec21d13cf86add58f6287cc`. * 🟡 (11:59) Verified merge base is exactly `ec83887a16f780f32fba4b7d710bad262dba3a22`. * 🟡 (11:59) Verified BASE commit/tree pair: `ec83887a16f780f32fba4b7d710bad262dba3a22 110d6530e57ca4fbfb2a3f98e…
Date: September 10, 2026 * 🔴 [requested-review] (11:57) User requested an independent READ-ONLY security merge gate for getsentry/cli PR #1569, reviewing only exact revisions BASE=ec83887a16f780f32fba4b7d710bad262dba3a22 and HEAD=9525f72c70fabbb23ec21d13cf86add58f6287cc in repository `/home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/toolkit-bridge-upgrade`. * 🔴 …