Dashboard › cli › Session 0W74UYEA2L1N
0W74UYEA2L1NQZL6fREAD-ONLY final correctness merge gate getsentry/cli PR #1569 exact base ec83887a16f780f32fba4b7d710bad262dba3a22 head e22f0eba66298a7228a988a4adcf1ec5be85227b. Inspect immutable Git objects only, never worktree. Repo /home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/toolkit-bridge-upgrade. Inspect every changed source/test. Verify all prior blockers fixed: primitive/object/Error abort reasons propagate unchanged through probes, token/manifest/tag/blob entry and already-aborted nightly; prefetched changelog accepts only branded same structural source and rejects raw/mismatch; stable targets always stable changelog, pinned nightly preserves tracking channel; Homebrew/npm target routing; semantic prerelease exclusion all stable paths/sources; exact metadata, 404-only, affinity, pagination, SHA/delta/cache/offline/API compatibility. Return substantive non-empty findings-first PASS/CONCERN/MUST-FIX/BLOCKED with exact head file:line and residual gaps. If no defects state PASS. Final line exactly MERGE or DO-NOT-MERGE. Never modify; empty forbidden.
true commit commit M packages/cli/src/commands/cli/upgrade.ts M packages/cli/src/lib/binary.ts M packages/cli/src/lib/delta-upgrade.ts M packages/cli/src/lib/ghcr.ts M packages/cli/src/lib/release-notes.ts M packages/cli/src/lib/upgrade.ts M packages/cli/src/lib/version-check.ts M packages/cli/test/commands/cli.test.ts M packages/cli/test/commands/cli/upgrade.test.ts M packages/cli/test/lib/binary.test.ts M packages/cli/test/lib/delta-upgrade.mocked.test.ts M packages/cli/test/lib/delta-upgrade.test.ts M packages/cli/test/lib/ghcr.test.ts M packages/cli/test/lib/release-notes.test.ts M packages/cli/test/lib/upgrade.test.ts M packages/cli/test/lib/version-check.test.ts packages/cli/src/commands/cli/upgrade.ts | 173 ++++-- packages/cli/src/lib/binary.ts | 157 +++++- packages/cli/src/lib/delta-upgrade.ts | 170 ++++-- packages/cli/src/lib/ghcr.ts | 106 +++- packages/cli/src/lib/release-notes.ts | 134 +++-- ...
# Jared (Outpost agent) Autonomous GitHub coding agent. Work in `/workspace/repo`. ## Model tiers The primary model is chosen per event (see `src/agents/models.ts`): heavy for code-producing situations, cheaper for lightweight ones. | Role | Subagent | Model | | --- | --- | --- | | Triage / plan / review (heavy) | (primary Jared) | Claude Opus 4.8 | | Triage / plan / review (light) | (primary Jared) | xAI Grok 4.3 | | Explore | `explore` | OpenAI gpt-5-mini | | Implement | `implement` | Moonshot kimi-k2.7-code | | Ship (commit/push/PR) | `ship` | xAI Grok (`grok-build-0.1`) | Pipeline: triage → explore → plan → implement → review → ship. (`worker` is a deprecated alias of `implement`.) Operators also talk to Jared directly from the Outpost dashboard. Those turns (`New operator chat` / `Operator guidance:`) skip triage — treat the request as the task and answer in the conversation. Long-term project knowledge for *this* Outpost repo lives in `.lore.md` when present. ...
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";
...
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 { stringifyUnknown, UpgradeError } from "./errors.js";
27 import { logger } from "./logger.js";
...
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. ...
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 ...
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 ...
...output truncated...
Full output saved to: /home/byk/.local/share/opencode/tool-output/tool_0897ac659001G6la9dykKMPo5P
150 throw new UpgradeError(
151 "network_error",
152 "GitHub returned an invalid release pagination URL"
153 );
154 }
155 const url = new URL(match[1]);
156 const isSelectedSourcePath =
157 url.pathname === `/repos/${source.githubRepo}/releases`;
158 const isCanonicalRepositoryPath = CANONICAL_RELEASES_PATH_REGEX.test(
159 url.pathname
160 );
161 const page = url.searchParams.get("page");
162 if (
163 url.protocol !== "https:" ||
164 url.hostname !== "api.github.com" ||
165 !(isSelectedSourcePath || isCanonicalRepositoryPath) ||
166 page === null ||
167 !PAGE_NUMBER_REGEX.test(page)
168 ) {
169 throw new UpgradeError(
170 "network_error",
171 "GitHub returned an invalid release pagination URL"
172 );
173 }
...
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 ...
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 ...
diff --git a/packages/cli/test/lib/binary.test.ts b/packages/cli/test/lib/binary.test.ts
index 6fcfb3b1a..be462848c 100644
--- a/packages/cli/test/lib/binary.test.ts
+++ b/packages/cli/test/lib/binary.test.ts
@@ -1,103 +1,229 @@
/**
* Binary Management Tests
*
* Tests for shared binary helpers: install directory selection, paths,
* download URLs, locking, and binary installation.
*/
import {
chmodSync,
mkdirSync,
readFileSync,
realpathSync,
rmSync,
symlinkSync,
writeFileSync,
} from "node:fs";
import { access, readFile, writeFile } from "node:fs/promises";
import { join, sep } from "node:path";
import { afterEach, beforeEach, describe, expect, test } from "vitest";
import {
acquireLock,
compareVersions,
determineInstallDir,
fetchWithUpgradeError,
getBinaryDownloadUrl,
getBinaryFilename,
getBinaryPaths,
+ getGitHubReleaseByTagUrl,
getLegacyInstallDirs,
getPlatformBinaryName,
installBinary,
isDowngrade,
...
diff --git a/packages/cli/test/lib/ghcr.test.ts b/packages/cli/test/lib/ghcr.test.ts
index e5c81d532..f2805f31a 100644
--- a/packages/cli/test/lib/ghcr.test.ts
+++ b/packages/cli/test/lib/ghcr.test.ts
@@ -1,47 +1,49 @@
/**
* 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;
/** Helper to mock fetch without ...
500 return new Response(JSON.stringify(manifest), {
501 status: 200,
502 headers: {
503 "Content-Type": "application/vnd.oci.image.manifest.v1+json",
504 },
505 });
506 });
507
508 const result = await fetchManifest("token", "patch-0.13.0");
509 expect(result).toEqual(manifest);
510 });
511
512 test("throws UpgradeError on HTTP 404", async () => {
513 mockFetch(async () => new Response("Not Found", { status: 404 }));
514
515 const error = await fetchManifest("token", "patch-0.13.0").catch(
516 (reason: unknown) => reason
517 );
518 expect(error).toBeInstanceOf(GhcrManifestHttpError);
519 expect(error).toMatchObject({
520 name: "GhcrManifestHttpError",
521 status: 404,
522 message: 'Failed to fetch manifest for tag "patch-0.13.0": HTTP 404',
523 });
524 });
525
526 test("throws UpgradeError ...
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
@@ -1,46 +1,52 @@
/**
* Unit Tests for Release Notes Parser & Aggregation
*
* Tests core invariants (section extraction, version filtering, truncation)
* that are hard to express as property-based tests due to format specifics.
*
* Core random-input invariants (category validity, filtering, commit parsing)
* are tested via property-based tests in release-notes.property.test.ts.
*/
import { marked } from "marked";
-import { describe, expect, test } from "vitest";
-import type { GitHubRelease } from "../../src/lib/delta-upgrade.js";
+import { afterEach, beforeEach, describe, expect, test } from "vitest";
+import { UPGRADE_SOURCES } from "../../src/lib/binary.js";
+import {
+ fetchRecentReleases,
+ type GitHubRelease,
+} from ...
138: fetchLatestFromGitHub,
139: fetchLatestFromNpm,
140: fetchLatestNightlyVersion,
141: fetchLatestVersion,
146: versionExists,
172: expect(parseInstallationMethod("brew")).toBe("brew");
173: expect(parseInstallationMethod("npm")).toBe("npm");
174: expect(parseInstallationMethod("pnpm")).toBe("pnpm");
180: expect(parseInstallationMethod("NPM")).toBe("npm");
192:describe("fetchLatestFromGitHub", () => {
201: { tag_name: "cli@99.0.0-dev.1", prerelease: false },
209: await expect(fetchLatestFromGitHub()).resolves.toBe("1.3.0");
215: test("follows Toolkit release pagination to find the latest CLI release", async () => {
239: await expect(fetchLatestFromGitHub()).resolves.toBe("1.2.3");
262: await expect(fetchLatestFromGitHub()).resolves.toBe("1.3.0");
266: test("rejects GitHub release pagination outside the selected source", async () => {
278: await expect(fetchLatestFromGitHub()).rejects.toThrow(
279: "GitHub returned an invalid release ...
190 });
191
192 describe("fetchLatestFromGitHub", () => {
193 test("selects the latest CLI-prefixed Toolkit release", async () => {
194 const requests: string[] = [];
195 mockFetch(async (url) => {
196 requests.push(String(url));
197 return new Response(
198 JSON.stringify([
199 { tag_name: "mcp@9.0.0" },
200 { tag_name: "cli@not-a-version" },
201 { tag_name: "cli@99.0.0-dev.1", prerelease: false },
202 { tag_name: "cli@1.2.3" },
203 { tag_name: "cli@1.3.0" },
204 ]),
205 { status: 200 }
206 );
207 });
208
209 await expect(fetchLatestFromGitHub()).resolves.toBe("1.3.0");
210 expect(requests).toEqual([
211 "https://api.github.com/repos/getsentry/toolkit/releases?per_page=100",
212 ]);
213 });
214
215 test("follows Toolkit release pagination to find the latest CLI release", async ...
42:} from "../../../src/lib/db/release-channel.js";
168: * Mock fetch to simulate GHCR manifest returning a specific nightly version.
187: // GHCR OCI manifest for :nightly tag
188: if (urlStr.includes("/manifests/nightly")) {
204: return new Response("Not Found", { status: 404 });
210: * Handles the latest release endpoint, version-exists check, and npm registry.
231: return new Response("Not Found", { status: 404 });
234: // npm registry fallback
235: if (new URL(urlStr).hostname === "registry.npmjs.org") {
242: return new Response("Not Found", { status: 404 });
247: * Mock fetch for the nightly version.json endpoint.
250: * Mock fetch for GHCR nightly version checks (token exchange + manifest).
251: * Used by nightly channel tests — replaces the old GitHub version.json mock.
265: if (urlStr.includes("/manifests/nightly")) {
273: return new Response("Not Found", { status: 404 });
298: test("shows the current and latest stable versions", async () => ...
280
281 beforeEach(() => {
282 testDir = join(
283 "/tmp",
284 `upgrade-cmd-test-${Date.now()}-${Math.random().toString(36).slice(2)}`
285 );
286 mkdirSync(testDir, { recursive: true });
287 originalFetch = globalThis.fetch;
288 });
289
290 afterEach(() => {
291 restoreStderr?.();
292 restoreStderr = undefined;
293 globalThis.fetch = originalFetch;
294 rmSync(testDir, { recursive: true, force: true });
295 });
296
297 describe("--check mode", () => {
298 test("shows the current and latest stable versions", async () => {
299 mockGitHubVersion("1.0.0");
300
301 const { context, getOutput, restore } = createMockContext({
302 homeDir: testDir,
303 });
304 restoreStderr = restore;
305
306 await run(
307 app,
308 ["cli", "upgrade", "--check", "--method", "curl"],
309 context
...
packages/cli/test/lib/delta-upgrade.test.ts:852: ])("excludes semantic prereleases from the %s stable source", async (_name, source, prereleaseTag, stableTag) => {
packages/cli/test/lib/delta-upgrade.test.ts:857: { ...makeRelease(prereleaseTag, []), prerelease: false },
packages/cli/test/lib/release-notes.test.ts:442: test("excludes semantic prereleases from stable changelogs", async () => {
packages/cli/test/lib/upgrade.test.ts:201: { tag_name: "cli@99.0.0-dev.1", prerelease: false },
packages/cli/test/lib/upgrade.test.ts:1865: expect(isNightlyVersion("2.0.0-beta.1")).toBe(false);
packages/cli/test/lib/version-check.test.ts:185: setVersionCheckInfo("0.0.0-alpha");
packages/cli/test/lib/version-check.test.ts:516: JSON.stringify([{ tag_name: "cli@99.0.0", draft: false }]),
04867eaf9b412a62d4b1ea1c59c2b382c557bd0b feat(cli): add toolkit upgrade bridge 9c874298e3b57515ba36d35140627a668a46dc7b fix(cli): address toolkit bridge review 723375b6ce4e37884e760fa0f848e7e7e3effcc9 fix(cli): address upgrade review findings dbee7e9f2a4555e044f74dfebad002cccfa43a1f test(cli): update toolkit upgrade fixtures 5ffa5cd0fe02c0a5dc5bec9a8a099f4135c0b447 fix(cli): harden upgrade source resolution c8c815bbecf472061ec02ab96d1a31f23f078d01 fix(cli): validate GitHub release pagination b6484e0fb1eb0f8bce68272d538f5c3b26cd5bd5 fix(cli): validate resolved upgrade metadata c6030fbf4d0c190f037c86d832edb6680d3e54ab fix(cli): bind nightly manifests to versions 2f9b5d0d26124d445a916b8bfc584ad518720584 fix(cli): isolate pinned upgrade resolution 4a1d5bad105c006193c68459175f5eb3c7f78e08 test(cli): update pinned version fixture e565b1afb39a341605adc883de8812851de7d581 fix(cli): validate stable upgrade metadata f1c10a6cb1a753b8c4b8c613cd78735c3f12cba6 fix(cli): preserve pinned upgrade ...
...output truncated...
Full output saved to: /home/byk/.local/share/opencode/tool-output/tool_0897d1d8b001Ipj5SZXboUCWtx
currentVersion: CLI_VERSION,
targetVersion: target,
offline,
source,
});
// --check with offline fallback: resolveTargetWithFallback returns
// kind: "target" for offline check, so guard against actual upgrade.
if (flags.check) {
return yield new CommandOutput(
await buildCheckResultWithChangelog({
target,
versionArg,
method,
channel,
flags,
offline,
changelogPromise,
})
);
}
// Skip if already on target — unless forced or switching channels
if (CLI_VERSION === target && !flags.force && !channelChanged) {
return yield new CommandOutput({
action: "up-to-date",
currentVersion: CLI_VERSION,
targetVersion: target,
channel,
method,
...
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
@@ -77,75 +77,75 @@ function versionHex(version: string): string {
.map((c) => c.charCodeAt(0).toString(16).padStart(2, "0"))
.join("");
}
function tempFile(name: string): string {
return join(
tmpdir(),
`delta-iso-${Date.now()}-${Math.random().toString(36).slice(2)}-${name}`
);
}
// ============================================================================
// resolveStableDelta
// ============================================================================
describe("resolveStableDelta", () => {
test("resolves and applies a stable delta patch", async () => {
// Create a "current binary" to patch from
const oldBinaryPath = tempFile("old-binary.bin");
const destPath = ...
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
@@ -97,140 +97,134 @@ describe("feedbackCommand.func", () => {
// Sentry is disabled in test environment (no DSN)
await expect(
func.call(mockContext, {}, "test", "feedback")
).rejects.toThrow("Feedback not sent: telemetry is disabled.");
});
});
// Test the upgrade command func
describe("upgradeCommand.func", () => {
let originalFetch: typeof globalThis.fetch;
let restoreStderr: (() => void) | undefined;
beforeEach(() => {
originalFetch = globalThis.fetch;
});
afterEach(() => {
restoreStderr?.();
restoreStderr = undefined;
globalThis.fetch = originalFetch;
});
// Note: We skip testing "unknown installation method" case because
// detectInstallationMethod() runs actual shell commands (npm ...
commit e22f0eba66298a7228a988a4adcf1ec5be85227b
Author: Burak Yigit Kaya <byk@sentry.io>
AuthorDate: Thu Sep 10 03:59:45 2026 +0000
Commit: Burak Yigit Kaya <byk@sentry.io>
CommitDate: Thu Sep 10 03:59:45 2026 +0000
fix(cli): preserve source and cancellation provenance
packages/cli/src/commands/cli/upgrade.ts | 11 +++------
packages/cli/src/lib/binary.ts | 3 +++
packages/cli/src/lib/ghcr.ts | 6 ++---
packages/cli/src/lib/release-notes.ts | 14 +++++------
packages/cli/src/lib/upgrade.ts | 4 +--
packages/cli/test/commands/cli/upgrade.test.ts | 14 ++++++++---
packages/cli/test/lib/binary.test.ts | 15 ++++++++++++
packages/cli/test/lib/ghcr.test.ts | 15 ++++++++++++
packages/cli/test/lib/release-notes.test.ts | 34 ++++++++++++++------------
packages/cli/test/lib/upgrade.test.ts | 9 +++++++
10 files changed, 87 insertions(+), 38 ...
390 const version = await fetchLatestFromGitHub();
391 expect(version).toBe("1.0.0");
392 });
393
394 test("throws on HTTP error", async () => {
395 mockFetch(
396 async () =>
397 new Response("Not Found", {
398 status: 404,
399 })
400 );
401
402 await expect(fetchLatestFromGitHub()).rejects.toThrow(UpgradeError);
403 await expect(fetchLatestFromGitHub()).rejects.toThrow(
404 "No CLI upgrade source was found: every source returned HTTP 404"
405 );
406 });
407
408 test("throws on network failure (DNS, timeout, etc.)", async () => {
409 mockFetch(async () => {
410 throw new TypeError("fetch failed");
411 });
412
413 await expect(fetchLatestFromGitHub()).rejects.toThrow(UpgradeError);
414 await expect(fetchLatestFromGitHub()).rejects.toThrow(
415 "Failed to connect to GitHub: fetch failed"
416 );
...
e565b1afb39a341605adc883de8812851de7d581 fix(cli): validate stable upgrade metadata packages/cli/src/commands/cli/upgrade.ts | 3 +- packages/cli/src/lib/upgrade.ts | 35 ++++++++++++++-- packages/cli/test/commands/cli.test.ts | 15 ++++--- packages/cli/test/commands/cli/upgrade.test.ts | 55 ++++++++++++++------------ packages/cli/test/lib/upgrade.test.ts | 55 +++++++++++++++++++++++++- 5 files changed, 124 insertions(+), 39 deletions(-) diff --git a/packages/cli/src/lib/upgrade.ts b/packages/cli/src/lib/upgrade.ts index 663adf772..4a23b4b25 100644 --- a/packages/cli/src/lib/upgrade.ts +++ b/packages/cli/src/lib/upgrade.ts @@ -1,59 +1,59 @@ /** * 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. ...
packages/cli/src/commands/cli/upgrade.ts:304: if (!(await versionExists(lookupMethod, target))) {
packages/cli/src/lib/upgrade.ts:817:export async function versionExists(
packages/cli/test/lib/upgrade.test.ts:712: await expect(versionExists("curl", "1.0.0")).resolves.toBe(true);
packages/cli/test/lib/upgrade.test.ts:727: await expect(versionExists("curl", "1.0.0")).resolves.toBe(true);
packages/cli/test/lib/upgrade.test.ts:809: versionExists("curl", "1.0.0", UPGRADE_SOURCES[0])
packages/cli/test/lib/upgrade.test.ts:822: versionExists("curl", "1.0.0", UPGRADE_SOURCES[0])
packages/cli/test/lib/upgrade.test.ts:832: const exists = await versionExists("curl", "1.0.0");
packages/cli/test/lib/upgrade.test.ts:839: const exists = await versionExists("curl", "99.99.99");
packages/cli/test/lib/upgrade.test.ts:846: const exists = await versionExists("npm", "1.0.0");
packages/cli/test/lib/upgrade.test.ts:853: const exists = await versionExists("npm", ...
packages/cli/src/lib/release-notes.ts:638: const { fromVersion, toVersion, maxItems, prefetchedReleases, source } =
packages/cli/src/lib/release-notes.ts:640: const releases = prefetchedReleases
packages/cli/src/lib/release-notes.ts:641: ? normalizeChangelogReleases(prefetchedReleases, source)
packages/cli/src/lib/release-notes.ts:746: prefetchedReleases?: NormalizedGitHubReleases;
packages/cli/src/lib/release-notes.ts:770: prefetchedReleases,
packages/cli/src/lib/release-notes.ts:786: prefetchedReleases,
packages/cli/test/lib/release-notes.test.ts:381: prefetchedReleases: releases,
packages/cli/test/lib/release-notes.test.ts:397: prefetchedReleases: releases,
packages/cli/test/lib/release-notes.test.ts:415: prefetchedReleases: [
packages/cli/test/lib/release-notes.test.ts:434: prefetchedReleases: [
packages/cli/test/lib/release-notes.test.ts:448: prefetchedReleases: [
diff --git a/packages/cli/test/lib/upgrade.test.ts b/packages/cli/test/lib/upgrade.test.ts
index 7c6f63a4b..9991222d3 100644
--- a/packages/cli/test/lib/upgrade.test.ts
+++ b/packages/cli/test/lib/upgrade.test.ts
@@ -115,16 +115,17 @@ vi.mock("node:child_process", async (importOriginal) => {
// Dynamic imports: must run AFTER vi.mock() so upgrade.ts picks up the
// mocked spawn.
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";
import { UpgradeError } from "../../src/lib/errors.js";
import { isProcessRunning } from "../../src/lib/process-utils.js";
@@ -135,16 +136,17 @@ const {
downloadBinaryToTemp,
executeUpgrade,
fetchLatestFromGitHub,
fetchLatestFromNpm,
fetchLatestNightlyVersion,
fetchLatestVersion,
...
b6484e0fb1eb0f8bce68272d538f5c3b26cd5bd5 fix(cli): validate resolved upgrade metadata
packages/cli/src/commands/cli/upgrade.ts | 16 +++----
packages/cli/src/lib/ghcr.ts | 10 ++++
packages/cli/src/lib/release-notes.ts | 20 ++++----
packages/cli/src/lib/upgrade.ts | 18 +++----
packages/cli/test/commands/cli/upgrade.test.ts | 65 ++++++++++++++++++++++----
packages/cli/test/lib/ghcr.test.ts | 12 +++++
packages/cli/test/lib/release-notes.test.ts | 7 +++
packages/cli/test/lib/upgrade.test.ts | 20 ++++++++
8 files changed, 131 insertions(+), 37 deletions(-)
diff --git a/packages/cli/src/lib/upgrade.ts b/packages/cli/src/lib/upgrade.ts
index 3f6c118ef..b669cd87b 100644
--- a/packages/cli/src/lib/upgrade.ts
+++ b/packages/cli/src/lib/upgrade.ts
@@ -467,74 +467,76 @@ export async function detectInstallationMethod(): Promise<InstallationMethod> {
});
} catch {
log.debug("Failed to persist ...
41dfbb7438cc2b86867afdf28edd92856a35254f commit
Blob cancellation can replace the caller’s abort reason.
/home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/toolkit-bridge-upgrade/packages/cli/src/lib/ghcr.ts:90-96
rethrowExternalAbort() recognizes a generic AbortError while the external signal is aborted, but throws the caught error rather than externalSignal.reason. Both blob request paths use this helper at lines 400–402 and 442–444.
Failure case:
Error reason RAbortError EisExternalAbort() returns trueE, losing RThis leaves the stated abort-provenance blocker unresolved for blob entry points.
Fix: throw externalSignal.reason whenever the helper identifies an external abort.
Regression: for both the initial GHCR blob request and redirected storage request, abort with separate primitive, object, and Error reasons while the fetch mock rejects a distinct AbortError; assert identity with rejects.toBe(reason) and one request per stage.
41dfbb7438cc2b86867afdf28edd92856a35254f.signal.reason.