Dashboard › cli › Session 0olc2V7J2Pvk
0olc2V7J2PvkP59rEREAD-ONLY replacement correctness gate for getsentry/cli PR #1569 exact immutable base ec83887a16f780f32fba4b7d710bad262dba3a22 head 878459c490576dbb7abb76d3a3dc1b5d9fc62c1f. Inspect only immutable Git objects (git diff base..head, git show head:path), never worktree. Repo /home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/toolkit-bridge-upgrade. You MUST return a substantive non-empty report. Inspect all changed source/tests. Verify transport-only UpgradeTransportError cache fallback, HTTP/metadata fail closed despite cache, stable npm/pnpm/bun/yarn malformed/prerelease rejection, arbitrary abort identity all paths, branded source provenance, target-driven channel/routing, semantic prerelease exclusion, exact metadata, 404-only fallback, source affinity, trusted pagination, redirect auth, SHA/delta/cache/offline/API compatibility. Findings first, each PASS/CONCERN/MUST-FIX/BLOCKED with exact head file:line and deterministic regression for defects; include residual gaps. Final exactly MERGE or DO-NOT-MERGE. Never modify. Empty output is forbidden.
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 | 167 +++++- packages/cli/src/lib/delta-upgrade.ts | 170 ++++-- packages/cli/src/lib/errors.ts | 8 + packages/cli/src/lib/ghcr.ts | ...
...output truncated...
Full output saved to: /home/byk/.local/share/opencode/tool-output/tool_0898dc051001SVnXgb3oAaSOl5
<!-- lore:019e6874-7d6f-7fb9-8085-ac2f3472e22f -->
* **sensitive argv flags must never reach telemetry — redactArgv() in cli.ts**: \`SENSITIVE\_ARGV\_FLAGS = new Set(\['token', 'auth-token'])\` in \`src/cli.ts\`. \`redactArgv()\` replaces values of these flags with \`\[REDACTED]\` before any telemetry call. This is an absolute invariant — never pass raw \`process.argv\` to telemetry without running through \`redactArgv()\` first.
<!-- lore:019e610e-c11c-7b34-ae65-02bf3183ae62 -->
* **setup.ts bestEffort() wrapper: post-install steps must never crash setup**: \`src/commands/cli/setup.ts\` \`bestEffort(stepName, fn)\` wraps non-essential post-install steps (recording install info, shell completions, agent skills) in try/catch. On failure: calls \`warn(stepName, error)\` + \`captureException(error, { level: 'warning', tags: { 'setup.step': stepName } })\`. ...
diff --git a/packages/cli/src/lib/upgrade.ts b/packages/cli/src/lib/upgrade.ts
index 945adc48d..06bd85646 100644
--- a/packages/cli/src/lib/upgrade.ts
+++ b/packages/cli/src/lib/upgrade.ts
@@ -1,169 +1,258 @@
/**
* 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..994626cfd 100644
--- a/packages/cli/src/lib/binary.ts
+++ b/packages/cli/src/lib/binary.ts
@@ -1,465 +1,612 @@
/**
* 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,388 +1,481 @@
/** 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,
...
...output truncated...
Full output saved to: /home/byk/.local/share/opencode/tool-output/tool_0898e61f3001rI322LDuTUoYKs
}
- // Validate that a specific pinned version actually exists.
- // Nightly builds are GitHub-only, so always use curl (GitHub) lookup for
- // nightly channel regardless of the current install method.
- if (versionArg && !CHANNEL_VERSIONS.has(versionArg)) {
- const lookupMethod = channel === "nightly" ? "curl" : method;
- const exists = await versionExists(lookupMethod, target);
- if (!exists) {
- throw new UpgradeError(
- "version_not_found",
- `Version ${target} not found`
- );
- }
- }
-
- return { kind: "target", target };
+ return { kind: "target", target: resolvedTarget, source };
}
/**
* Build the structured result for check-only mode. ...
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";
...
878459c490576dbb7abb76d3a3dc1b5d9fc62c1f:packages/cli/test/commands/cli/upgrade.test.ts:205: return new Response("Not Found", { status: 404 });
878459c490576dbb7abb76d3a3dc1b5d9fc62c1f:packages/cli/test/commands/cli/upgrade.test.ts:232: return new Response("Not Found", { status: 404 });
878459c490576dbb7abb76d3a3dc1b5d9fc62c1f:packages/cli/test/commands/cli/upgrade.test.ts:243: return new Response("Not Found", { status: 404 });
878459c490576dbb7abb76d3a3dc1b5d9fc62c1f:packages/cli/test/commands/cli/upgrade.test.ts:274: return new Response("Not Found", { status: 404 });
878459c490576dbb7abb76d3a3dc1b5d9fc62c1f:packages/cli/test/commands/cli/upgrade.test.ts:367: return new Response("Not Found", { status: 404 });
878459c490576dbb7abb76d3a3dc1b5d9fc62c1f:packages/cli/test/commands/cli/upgrade.test.ts:406: test("uses the cached target only after a transport failure", async () => ...
360 const requests: string[] = [];
361 mockFetch(async (url) => {
362 const request = String(url);
363 requests.push(request);
364 if (
365 request.includes("getsentry/toolkit/releases/tags/cli%4088.88.88")
366 ) {
367 return new Response("Not Found", { status: 404 });
368 }
369 if (request.includes("getsentry/cli/releases/tags/88.88.88")) {
370 return new Response(JSON.stringify({ tag_name: "88.88.88" }), {
371 status: 200,
372 });
373 }
374 if (request.includes("getsentry/cli/releases?per_page=30")) {
375 return new Response(JSON.stringify([]), { status: 200 });
376 }
377 return new Response("Unexpected", { status: 500 });
378 });
379
380 const { context, restore } = createMockContext({ homeDir: testDir });
381 restoreStderr = restore;
382
...
150
151 // Store original fetch for restoration
152 let originalFetch: typeof globalThis.fetch;
153
154 /** Helper to mock fetch without TypeScript errors about missing Bun-specific properties */
155 function mockFetch(
156 fn: (url: string | URL | Request, init?: RequestInit) => Promise<Response>
157 ): void {
158 globalThis.fetch = fn as typeof globalThis.fetch;
159 }
160
161 beforeEach(() => {
162 originalFetch = globalThis.fetch;
163 });
164
165 afterEach(() => {
166 globalThis.fetch = originalFetch;
167 });
168
169 describe("parseInstallationMethod", () => {
170 test("parses valid methods", () => {
171 expect(parseInstallationMethod("curl")).toBe("curl");
172 expect(parseInstallationMethod("brew")).toBe("brew");
173 expect(parseInstallationMethod("npm")).toBe("npm");
174 expect(parseInstallationMethod("pnpm")).toBe("pnpm");
175 ...
878459c490576dbb7abb76d3a3dc1b5d9fc62c1f:packages/cli/test/commands/cli/upgrade.test.ts:428: "malformed HTTP 200",
878459c490576dbb7abb76d3a3dc1b5d9fc62c1f:packages/cli/test/lib/upgrade.test.ts:139: fetchLatestFromNpm,
878459c490576dbb7abb76d3a3dc1b5d9fc62c1f:packages/cli/test/lib/upgrade.test.ts:284: test("classifies malformed GitHub release pagination as a network error", async () => {
878459c490576dbb7abb76d3a3dc1b5d9fc62c1f:packages/cli/test/lib/upgrade.test.ts:434:describe("fetchLatestFromNpm", () => {
878459c490576dbb7abb76d3a3dc1b5d9fc62c1f:packages/cli/test/lib/upgrade.test.ts:449: const version = await fetchLatestFromNpm();
878459c490576dbb7abb76d3a3dc1b5d9fc62c1f:packages/cli/test/lib/upgrade.test.ts:461: await expect(fetchLatestFromNpm()).rejects.toThrow(UpgradeError);
878459c490576dbb7abb76d3a3dc1b5d9fc62c1f:packages/cli/test/lib/upgrade.test.ts:462: await ...
430 );
431 });
432 });
433
434 describe("fetchLatestFromNpm", () => {
435 test("returns version from npm registry", async () => {
436 mockFetch(
437 async () =>
438 new Response(
439 JSON.stringify({
440 version: "1.2.3",
441 }),
442 {
443 status: 200,
444 headers: { "Content-Type": "application/json" },
445 }
446 )
447 );
448
449 const version = await fetchLatestFromNpm();
450 expect(version).toBe("1.2.3");
451 });
452
453 test("throws on HTTP error", async () => {
454 mockFetch(
455 async () =>
456 new Response("Server Error", {
457 status: 500,
458 })
459 );
460
461 await expect(fetchLatestFromNpm()).rejects.toThrow(UpgradeError);
462 await expect(fetchLatestFromNpm()).rejects.toThrow(
463 "Failed to ...
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,60 +1,64 @@
/**
* 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,
...
90 getProbeUrl: (source) => getGitHubReleaseByTagUrl("0.45.0", source),
91 fetch: async (url) => {
92 requests.push(String(url));
93 return new Response(JSON.stringify({ tag_name: "cli@0.45.0" }), {
94 status: 200,
95 });
96 },
97 });
98
99 expect(resolved).toEqual({
100 source: UPGRADE_SOURCES[0],
101 response: expect.any(Response),
102 });
103 expect(requests).toEqual([
104 "https://api.github.com/repos/getsentry/toolkit/releases/tags/cli%400.45.0",
105 ]);
106 });
107
108 test("falls back to the legacy source only on HTTP 404", async () => {
109 const requests: string[] = [];
110
111 const resolved = await resolveUpgradeSource({
112 getProbeUrl: (source) => getGitHubReleaseByTagUrl("0.45.0", source),
113 fetch: async (url) => {
114 requests.push(String(url));
115 ...
...output truncated...
Full output saved to: /home/byk/.local/share/opencode/tool-output/tool_08990bc0c00153g7U6ec7Y74I7
",
+ status: 404,
+ message: 'Failed to fetch manifest for tag "patch-0.13.0": HTTP 404',
+ });
});
test("throws UpgradeError on network failure", async () => {
mockFetch(async () => {
throw new TypeError("fetch failed");
});
await expect(fetchManifest("token", "some-tag")).rejects.toThrow(
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. ...
diff --git a/packages/cli/test/commands/cli/upgrade.test.ts b/packages/cli/test/commands/cli/upgrade.test.ts
index ca9688e46..3494c02a8 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 ...
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
@@ -97,14 +97,14 @@ describe("resolveStableDelta", () => {
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 = `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: [
{
...
878459c490576dbb7abb76d3a3dc1b5d9fc62c1f fix(cli): fail closed on upgrade metadata --- e22f0eba66298a7228a988a4adcf1ec5be85227b fix(cli): preserve source and cancellation provenance --- dcf6719995800d28d6f88a8c709abe4aa3d0b6db test(cli): validate GitHub request origin --- f1c10a6cb1a753b8c4b8c613cd78735c3f12cba6 fix(cli): preserve pinned upgrade contracts --- e565b1afb39a341605adc883de8812851de7d581 fix(cli): validate stable upgrade metadata --- 4a1d5bad105c006193c68459175f5eb3c7f78e08 test(cli): update pinned version fixture --- 2f9b5d0d26124d445a916b8bfc584ad518720584 fix(cli): isolate pinned upgrade resolution --- c6030fbf4d0c190f037c86d832edb6680d3e54ab fix(cli): bind nightly manifests to versions --- b6484e0fb1eb0f8bce68272d538f5c3b26cd5bd5 fix(cli): validate resolved upgrade metadata --- c8c815bbecf472061ec02ab96d1a31f23f078d01 fix(cli): validate GitHub release pagination --- 5ffa5cd0fe02c0a5dc5bec9a8a099f4135c0b447 fix(cli): harden upgrade source ...
...output truncated...
Full output saved to: /home/byk/.local/share/opencode/tool-output/tool_0899222a30010p7wjaZHuq1lJf
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";
import {
getReleaseChannel,
type ReleaseChannel,
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 "../../lib/formatters/numbers.js";
import { CommandOutput } from "../../lib/formatters/output.js";
import { logger } from "../../lib/logger.js";
import { ...
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. ...
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 ...
420 if (!redirectUrl) {
421 throw new UpgradeError(
422 "network_error",
423 `GHCR blob redirect (${blobResponse.status}) had no Location header`
424 );
425 }
426
427 // Step 2: Follow the redirect WITHOUT the Authorization header.
428 // Azure rejects requests that include a Bearer token alongside its own
429 // signed query-string credentials (returns 404).
430 // No AbortSignal.timeout here: this fetch covers both connection AND
431 // body streaming. For full nightly binaries (~30 MB), a 30s timeout
432 // would require sustained ~8 Mbps throughput and fail on slow connections.
433 // The GHCR step 1 timeout above guards against GHCR-side latency;
434 // Azure Blob Storage has reliable latency characteristics. ...
455 return stored.method;
456 }
457
458 // Legacy detection for existing installs (pre-setup command)
459 const legacyMethod = await detectLegacyInstallationMethod();
460
461 // Auto-save detected method for future runs (best-effort —
462 // a read-only or broken DB shouldn't block detection)
463 if (legacyMethod !== "unknown") {
464 try {
465 setInstallInfo({
466 method: legacyMethod,
467 path: process.execPath,
468 version: CLI_VERSION,
469 });
470 } catch {
471 log.debug("Failed to persist install info (DB may be read-only)");
472 }
473 }
474
475 return legacyMethod;
476 }
477
478 // Version Fetching
479
480 /**
481 * Fetch the latest version from GitHub releases. ...
741bd72559d1033c0f801cba98e635f85f0b96dcca6f2180f5d9158ffb5251fc -
fetchLatestFromGitHubWithSource() accepts either an object or an array for every source:
/home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/toolkit-bridge-upgrade/packages/cli/src/lib/upgrade.ts:501-504This accepts malformed but plausible metadata:
{tag_name:"cli@9.9.9"} instead of an array./releases/latest endpoint can return [{tag_name:"9.9.9"}] instead of an object.Both responses currently select and install 9.9.9, violating the fail-closed exact-metadata contract.
The explicit-source existence path also treats every successful HTTP response as proof without parsing its tag:
/home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/toolkit-bridge-upgrade/packages/cli/src/lib/upgrade.ts:797-803An empty HTTP 200 therefore makes versionExists("curl", version, source) return true.
Fix: Validate endpoint-specific response shapes and exact tags. Toolkit list responses must be arrays; legacy /latest responses must be one release object. The explicit-source existence path must parse and compare tag_name with ${source.tagPrefix}${version}.
Deterministic regressions:
{tag_name:"cli@9.9.9"} from Toolkit’s list endpoint and assert rejection, no cache fallback, and no download.[{tag_name:"9.9.9"}] from legacy /latest; assert terminal rejection.versionExists(..., UPGRADE_SOURCES[0]); assert rejection rather than true.fetchWithUpgradeError() only preserves errors named AbortError:
/home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/toolkit-bridge-upgrade/packages/cli/src/lib/binary.ts:500-514It does not inspect ...
* 🔴 (04:29) GHCR binary downloads use a two-step redirect flow: if the initial blob response has no `Location` header, code throws `new UpgradeError("network_error", \`GHCR blob redirect (${blobResponse.status}) had no Location header\`)`; the redirect is then followed with `customFetch(redirectUrl, { headers: { "User-Agent": getUserAgent() }, signal })` and deliberately without the `Authorizati…
* 🔴 (04:28) User stated package managers always need network access to fetch and install packages; `validateMethod()` therefore permits offline mode only when `method === "curl"` and rejects offline use with other installation methods. * 🔴 (04:28) `stripTrailingSep(p)` strips a trailing path separator only when `p.length > 1`; it never strips the separator from a bare root such as `/`, while al…
* 🔴 (04:27) `packages/cli/test/commands/cli/upgrade.test.ts` now imports `setVersionCheckInfo` from `../../../src/lib/db/version-check.js` to seed cached version-check state in upgrade tests. * 🔴 (04:27) Upgrade test helpers `mockGhcrNightlyVersion()` and `mockNightlyVersion()` now allow the Toolkit source probe `https://api.github.com/repos/getsentry/toolkit` to return HTTP 200 before handling…
* 🔴 (04:26) A `fetchManifest("token", "patch-0.13.0")` HTTP 404 test now verifies structured error details including `status: 404` and `message: 'Failed to fetch manifest for tag "patch-0.13.0": HTTP 404'`. * 🔴 (04:26) `packages/cli/test/lib/release-notes.test.ts` now imports `UPGRADE_SOURCES`, `fetchRecentReleases`, `fetchChangelog`, and `mockFetch`, and adds `beforeEach`/`afterEach` lifecycle…
* 🔴 (04:25) `packages/cli/src/lib/version-check.ts` now imports `UpgradeSource` from `./binary.js` and uses `fetchLatestFromGitHubWithSource` / `fetchLatestNightlyVersionWithSource` from `./upgrade.js`, allowing version discovery to retain the selected upgrade source. * 🔴 (04:25) Background version checks pass the discovered `source` into `maybePrefetchPatches(channel, latestVersion, signal, so…
* 🔴 (04:24) User’s `packages/cli/test/commands/cli/upgrade.test.ts` verifies pinned stable resolution for `88.88.88`: request `https://api.github.com/repos/getsentry/toolkit/releases/tags/cli%4088.88.88`; after its `HTTP 404`, request `https://api.github.com/repos/getsentry/cli/releases/tags/88.88.88`; then request `https://api.github.com/repos/getsentry/cli/releases?per_page=30`. It must not re…
* 🔴 (04:24) User established upgrade cache behavior in `packages/cli/test/commands/cli/upgrade.test.ts`: cached target is used only after a transport failure (`"Using cached target: 88.88.88"`), and the test `"never uses the cached target after %s"` verifies it is never used after `HTTP 403` or a `malformed HTTP 200`. * 🔴 (04:24) User’s test suite specifies in `packages/cli/test/lib/binary.test…
Date: Sep 10, 2026 * 🔴 (04:23) User stated package managers always require network access to fetch and install packages.
* 🔴 (04:23) User specified that nightly builds are GitHub-only and pinned nightly-version existence checks must always use the `"curl"` installation method for GitHub lookup, regardless of the current install method. * 🔴 (04:23) User documented `sentry cli upgrade nightly` as the command to “Switch to nightly channel and update.” * 🔴 (04:23) User changed target resolution to return `{ kind: "t…
Date: Sep 10, 2026 * 🔴 (04:23) User changed `packages/cli/src/lib/delta-upgrade.ts` to import `prerelease as semverPrerelease` and `valid as semverValid` from `semver`, supporting source-aware release normalization. * 🔴 (04:23) User changed `packages/cli/src/lib/delta-upgrade.ts` imports from `./binary.js`: replaced `GITHUB_RELEASES_URL` with `getGitHubReleasesUrl`, and added `PRIMARY_UPGRADE_S…
* 🔴 (04:23) User provided changes to `packages/cli/src/lib/binary.ts` importing `UpgradeTransportError` alongside `stringifyUnknown` and `UpgradeError` from `./errors.js`. * 🔴 (04:23) User stated `stripTrailingSep(p: string)` strips a trailing path separator only when `p.length > 1`, never from a bare root like `/`, so PATH entries such as `~/.local/bin/` and `~/.local/bin` compare equally. * �…
* 🔴 (04:23) User stated installation detection must always check for Homebrew first because stored install info may be stale; `detectInstallationMethod()` uses the cheap, authoritative `isHomebrewInstall()` realpath check before consulting stored DB information. * 🔴 (04:23) User provided installation-method priority in `packages/cli/src/lib/upgrade.ts`: 1. Homebrew; 2. stored install info in DB…
Date: Sep 10, 2026 * 🔴 [requested-review] (04:22) User requested a read-only replacement-correctness gate for getsentry/cli PR #1569 using exact immutable base `ec83887a16f780f32fba4b7d710bad262dba3a22` and head `878459c490576dbb7abb76d3a3dc1b5d9fc62c1f` in repo `/home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/toolkit-bridge-upgrade`. * 🔴 [enforced-read-only-re…