Dashboard › cli › Session 0DYKj4ZJRgV2
0DYKj4ZJRgV24NQKYREAD-ONLY final security/supply-chain 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 all changed files/tests. Verify prior blockers: primitive/object/Error abort reasons exact through probes/GHCR/already-aborted; strict branded same-source prefetched release provenance, raw/mismatch rejection; stable target stable changelog while tracking nightly; pinned nightly channel preservation; target-driven Homebrew/npm; stable semantic prerelease exclusion every source/path; exact metadata; typed 404-only/fail closed; affinity/canonical pagination/GHCR scope+redirect/SHA/delta/cache/offline/installer unchanged; CodeQL URL assertion uses exact parsed origin. Return substantive non-empty findings-first PASS/CONCERN/MUST-FIX/BLOCKED exact head file:line and residual gaps. Final exactly MERGE or DO-NOT-MERGE. Never modify; empty forbidden.
110d6530e57ca4fbfb2a3f98e52013d2668b5b5e 39711200f27238a7669dfd3444fa65b8701c346e 41dfbb7438cc2b86867afdf28edd92856a35254f
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 +++-- ...
.lore.md AGENTS.md package.json packages/cli/AGENTS.md packages/cli/CONTRIBUTING.md packages/cli/package.json
...output truncated...
Full output saved to: /home/byk/.local/share/opencode/tool-output/tool_0897ab5be0018s15Y3jAUbNkBz
### Quick API Reference
| Task | Use This |
|------|----------|
| Read file | `await readFile(path, "utf-8")` (`node:fs/promises`) |
| Write file | `await writeFile(path, content)` (`node:fs/promises`) |
| Check file exists | `existsSync(path)` (`node:fs`) |
| Spawn process | `spawn()` / `execFile()` (`node:child_process`) |
| Shell commands | `execSync()` (`node:child_process`) |
| Find executable | `which` helper in `src/lib/which.ts` |
| Glob patterns | the scanner in `src/lib/scan/` |
| Sleep | `await setTimeout(ms)` (`node:timers/promises`) |
| Parse JSON file | `JSON.parse(await readFile(path, "utf-8"))` |
Use `node:fs` for directory creation with permissions:
```typescript
import { mkdirSync } from "node:fs";
mkdirSync(dir, { recursive: true, mode: 0o700 });
```
For shell commands, prefer `execFileSync` with array args over `execSync` with a
string when ...
...output truncated... Full output saved to: /home/byk/.local/share/opencode/tool-output/tool_0897ada600019aLHS3JIomPK5m <!-- lore:019edfc4-56e4-7207-90de-8488c48a030b --> * **getsentry/symbolic: Rust test conventions — unwrap() not -> Result**: Per Dav1dde review on PR #988: test functions in \`symbolic-debuginfo/tests/\` must NOT return \`Result\` — use \`unwrap()\` so stack traces point to the assert location. If a shared error type is needed, define a module-level type alias: \`type Result\<T, E = Box\<dyn std::error::Error>> = std::result::Result\<T, E>;\` but still prefer \`unwrap()\` in test bodies. <!-- lore:019ee000-c330-7ac9-9c98-24f2a8655026 --> * **getsentry/symbolic: wasm smoke test pattern — smoke-test.mjs + build-npm.sh + ci.yml wasm-smoke job**: symbolic-wasm smoke test pattern: Two-file approach in \`symbolic-wasm/npm/\`: \`smoke-test.mjs\` (orchestrator: packs tgz, installs to temp dir, resolves wasm via exports map, spawns \`node --test\` on ...
diff --git a/packages/cli/src/commands/cli/upgrade.ts b/packages/cli/src/commands/cli/upgrade.ts index 8d8282368..39aa97928 100644 --- a/packages/cli/src/commands/cli/upgrade.ts +++ b/packages/cli/src/commands/cli/upgrade.ts @@ -1,414 +1,468 @@ /** * 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/upgrade.ts b/packages/cli/src/lib/upgrade.ts
index 945adc48d..a6fad4e9e 100644
--- a/packages/cli/src/lib/upgrade.ts
+++ b/packages/cli/src/lib/upgrade.ts
@@ -1,189 +1,278 @@
/**
* Upgrade Module
*
* Detects how the CLI was installed and provides self-upgrade functionality.
* Binary management helpers (download URLs, locking, replacement) live in
* binary.ts and are shared with the setup --install flow.
*/
import { spawn } from "node:child_process";
import {
chmodSync,
closeSync,
existsSync,
openSync,
realpathSync,
statSync,
unlinkSync,
writeSync,
} from "node:fs";
import { writeFile } from "node:fs/promises";
import { homedir } from "node:os";
import { dirname, isAbsolute, join, sep } from "node:path";
import { setTimeout } from "node:timers/promises";
+import { prerelease as semverPrerelease, valid as semverValid } from "semver";
import {
acquireLock,
cleanupOldBinary,
+ compareVersions,
...
diff --git a/packages/cli/src/lib/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,
...
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,142 +1,149 @@ /** * 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()`. ...
===== packages/cli/test/commands/cli.test.ts =====
65:describe("feedbackCommand.func", () => {
66: test("throws ValidationError for empty message", async () => {
79: test("throws ValidationError for whitespace-only message", async () => {
91: test("throws ConfigError when Sentry is disabled", async () => {
106:describe("upgradeCommand.func", () => {
125: test("shows installation info with specified method", async () => {
146: test("check mode shows update available", async () => {
166: test("check mode with version shows versioned command", async () => {
194: test("check mode compares the current version with a stable target", async () => {
213: test("throws UpgradeError when specified version does not exist", async () => {
===== packages/cli/test/commands/cli/upgrade.test.ts =====
277:describe("sentry cli upgrade", () => {
297: describe("--check mode", () => {
298: test("shows the current and latest stable versions", async () => {
318: test("shows upgrade command ...
e22f0eba66298a7228a988a4adcf1ec5be85227b:packages/cli/src/commands/cli/upgrade.ts:10: * - nightly: tracks the rolling nightly prerelease built from main
e22f0eba66298a7228a988a4adcf1ec5be85227b:packages/cli/src/lib/delta-upgrade.ts:33:import { prerelease as semverPrerelease, valid as semverValid } from "semver";
e22f0eba66298a7228a988a4adcf1ec5be85227b:packages/cli/src/lib/delta-upgrade.ts:106: .filter((release) => !(release.draft || release.prerelease))
e22f0eba66298a7228a988a4adcf1ec5be85227b:packages/cli/src/lib/delta-upgrade.ts:202: prerelease?: boolean;
e22f0eba66298a7228a988a4adcf1ec5be85227b:packages/cli/src/lib/upgrade.ts:24:import { prerelease as semverPrerelease, valid as semverValid } from "semver";
e22f0eba66298a7228a988a4adcf1ec5be85227b:packages/cli/src/lib/upgrade.ts:120: | { tag_name?: string; draft?: boolean; prerelease?: boolean }
e22f0eba66298a7228a988a4adcf1ec5be85227b:packages/cli/src/lib/upgrade.ts:121: | Array<{ tag_name?: string; draft?: boolean; ...
109- 109 ...orig,
110- 110 spawn: (cmd: string, args: string[], opts: object) =>
111- 111 spawnImpl.fn(cmd, args, opts),
112- 112 };
113- 113 });
114- 114
115- 115 // Dynamic imports: must run AFTER vi.mock() so upgrade.ts picks up the
116- 116 // mocked spawn.
117- 117 import { isEnoentSpawnError } from "../../src/commands/cli/upgrade.js";
118- 118 import {
119- 119 acquireLock,
120- 120 getBinaryDownloadUrl,
121- 121 isNightlyVersion,
122- 122 releaseLock,
123- 123 UPGRADE_SOURCES,
124- 124 } from "../../src/lib/binary.js";
125- 125 import {
126- 126 clearInstallInfo,
127- 127 setInstallInfo,
128- 128 } from "../../src/lib/db/install-info.js";
129- 129 import { UpgradeError } from "../../src/lib/errors.js";
130- 130 import { isProcessRunning } from "../../src/lib/process-utils.js";
131- 131
132- 132 const {
133- 133 buildKnownCurlPaths,
134- 134 detectInstallationMethod,
135- 135 ...
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_0897cd490001NUYh0ouJf4sSfI
export function getGitHubRepositoryUrl(
source: UpgradeSource = PRIMARY_UPGRADE_SOURCE
): string {
return `https://api.github.com/repos/${source.githubRepo}`;
}
/** GitHub API base URL for the primary release source. */
export const GITHUB_RELEASES_URL = getGitHubReleasesUrl();
/** Result of selecting one source for an upgrade operation. */
export type ResolvedUpgradeSource = {
/** The selected release source. */
readonly source: UpgradeSource;
/** The successful response from the source probe. */
readonly response: Response;
};
/** All configured upgrade sources returned an HTTP 404 response. */
export class UpgradeSourceNotFoundError extends UpgradeError {
constructor() {
super(
"network_error",
"No CLI upgrade source was found: every source returned HTTP 404"
);
this.name = ...
101- 101 });
102- 102 });
103- 103
104- 104 await expect(getAnonymousToken(UPGRADE_SOURCES[0])).resolves.toBe(
105- 105 "toolkit-token"
106- 106 );
107- 107 });
108- 108
109- 109 test("throws UpgradeError on HTTP error", async () => {
110- 110 mockFetch(async () => new Response("Unauthorized", { status: 401 }));
111- 111
112- 112 await expect(getAnonymousToken()).rejects.toThrow(UpgradeError);
113- 113 await expect(getAnonymousToken()).rejects.toThrow(
114- 114 "GHCR token exchange failed: HTTP 401"
115- 115 );
116- 116 });
117- 117
118- 118 test("throws UpgradeError on network failure", async () => {
119- 119 mockFetch(async () => {
120- 120 throw new TypeError("fetch failed");
121- 121 });
122- 122
123- 123 await expect(getAnonymousToken()).rejects.toThrow(UpgradeError);
124- 124 await expect(getAnonymousToken()).rejects.toThrow(
125- 125 "Failed ...
string true undefined undefined object true undefined undefined object true Error e
e22f0eba66298a7228a988a4adcf1ec5be85227b:packages/cli/src/lib/binary.ts:493:export async function fetchWithUpgradeError(
e22f0eba66298a7228a988a4adcf1ec5be85227b:packages/cli/src/lib/custom-ca.ts:305:export function customFetch(
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. ...
236- 236 });
237- 237 });
238- 238
239- 239 await expect(fetchLatestFromGitHub()).resolves.toBe("1.2.3");
240- 240 expect(requests).toEqual([
241- 241 "https://api.github.com/repos/getsentry/toolkit/releases?per_page=100",
242- 242 "https://api.github.com/repos/getsentry/toolkit/releases?per_page=100&page=2",
243- 243 ]);
244- 244 });
245- 245
246- 246 test("selects the highest CLI SemVer across Toolkit release pages", async () => {
247- 247 let requests = 0;
248- 248 mockFetch(async () => {
249- 249 requests += 1;
250- 250 return requests === 1
251- 251 ? new Response(JSON.stringify([{ tag_name: "cli@1.2.1" }]), {
252- 252 status: 200,
253- 253 headers: {
254- 254 Link: '<https://api.github.com/repositories/1114546946/releases?per_page=100&page=2>; rel="next"',
255- 255 },
256- 256 })
257- 257 : new ...
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/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/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,
...
71- 71 * The implementation is chosen once at module load from
72- 72 * {@link IS_CASE_INSENSITIVE_FS} so there is no per-call platform check.
73- 73 */
74- 74 export const samePath: (a: string, b: string) => boolean =
75- 75 IS_CASE_INSENSITIVE_FS
76- 76 ? (a, b) =>
77- 77 stripTrailingSep(a).toLowerCase() === stripTrailingSep(b).toLowerCase()
78- 78 : (a, b) => stripTrailingSep(a) === stripTrailingSep(b);
79- 79
80- 80 /**
81- 81 * Absolute legacy install directories for the given home. See
82- 82 * {@link LEGACY_INSTALL_SUBDIRS} for why this is scoped to pre-XDG locations.
83- 83 */
84- 84 export function getLegacyInstallDirs(homeDir: string): string[] {
85- 85 return LEGACY_INSTALL_SUBDIRS.map((dir) => join(homeDir, dir));
86- 86 }
87- 87
88- 88 /**
89- 89 * How the CLI was installed. Determines the upgrade strategy. ...
e22f0eba66298a7228a988a4adcf1ec5be85227b:packages/cli/src/commands/cli/upgrade.ts:233: log.warn("Network unavailable, falling back to cached upgrade target");
e22f0eba66298a7228a988a4adcf1ec5be85227b:packages/cli/src/commands/cli/upgrade.ts:235: return { kind: "target", target, offline: "network-fallback" };
e22f0eba66298a7228a988a4adcf1ec5be85227b:packages/cli/src/lib/upgrade.ts:86: * - `"network-fallback"` — network failed, auto-fell back to cache
e22f0eba66298a7228a988a4adcf1ec5be85227b:packages/cli/src/lib/upgrade.ts:88:export type OfflineMode = false | "explicit" | "network-fallback";
e22f0eba66298a7228a988a4adcf1ec5be85227b:packages/cli/test/lib/db/repo-cache.test.ts:2: * Tests for the Sentry repository offline cache.
e22f0eba66298a7228a988a4adcf1ec5be85227b:packages/cli/test/lib/resolve-effective-org.test.ts:4: * Covers the offline cache lookup path that resolves DSN-style org
e22f0eba66298a7228a988a4adcf1ec5be85227b:packages/cli/test/lib/upgrade.test.ts:2225: ...
e22f0eba66298a7228a988a4adcf1ec5be85227b:packages/cli/src/lib/release-notes.ts:588: * Uses a higher `per_page` than `fetchRecentReleases()` in delta-upgrade
e22f0eba66298a7228a988a4adcf1ec5be85227b:packages/cli/src/lib/release-notes.ts:638: const { fromVersion, toVersion, maxItems, prefetchedReleases, source } =
e22f0eba66298a7228a988a4adcf1ec5be85227b:packages/cli/src/lib/release-notes.ts:640: const releases = prefetchedReleases
e22f0eba66298a7228a988a4adcf1ec5be85227b:packages/cli/src/lib/release-notes.ts:641: ? normalizeChangelogReleases(prefetchedReleases, source)
e22f0eba66298a7228a988a4adcf1ec5be85227b:packages/cli/src/lib/release-notes.ts:746: prefetchedReleases?: NormalizedGitHubReleases;
e22f0eba66298a7228a988a4adcf1ec5be85227b:packages/cli/src/lib/release-notes.ts:770: prefetchedReleases,
e22f0eba66298a7228a988a4adcf1ec5be85227b:packages/cli/src/lib/release-notes.ts:786: ...
531- 531 `Failed to fetch from GitHub: HTTP ${response.status}`
532- 532 );
533- 533 }
534- 534 }
535- 535 }
536- 536
537- 537 /** Fetch the latest standalone CLI version from the ordered GitHub sources. */
538- 538 export async function fetchLatestFromGitHub(
539- 539 signal?: AbortSignal,
540- 540 source?: UpgradeSource
541- 541 ): Promise<string> {
542- 542 return (
543- 543 await fetchLatestFromGitHubWithSource(
544- 544 signal,
545- 545 source ? [source] : UPGRADE_SOURCES
546- 546 )
547- 547 ).version;
548- 548 }
549- 549
550- 550 /**
551- 551 * Fetch the latest version from npm registry. ...
e22f0eba66298a7228a988a4adcf1ec5be85227b:packages/cli/src/lib/upgrade.ts:24:import { prerelease as semverPrerelease, valid as semverValid } from "semver";
e22f0eba66298a7228a988a4adcf1ec5be85227b:packages/cli/src/lib/upgrade.ts:120: | { tag_name?: string; draft?: boolean; prerelease?: boolean }
e22f0eba66298a7228a988a4adcf1ec5be85227b:packages/cli/src/lib/upgrade.ts:121: | Array<{ tag_name?: string; draft?: boolean; prerelease?: boolean }>,
e22f0eba66298a7228a988a4adcf1ec5be85227b:packages/cli/src/lib/upgrade.ts:126: .filter((release) => !(release.draft || release.prerelease))
e22f0eba66298a7228a988a4adcf1ec5be85227b:packages/cli/src/lib/upgrade.ts:503: | Array<{ tag_name?: string; draft?: boolean; prerelease?: boolean }>;
e22f0eba66298a7228a988a4adcf1ec5be85227b:packages/cli/src/lib/upgrade.ts:1165: * so the URL points to the prerelease assets regardless of the version string.
e22f0eba66298a7228a988a4adcf1ec5be85227b:packages/cli/test/lib/upgrade.test.ts:201: ...
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(-) 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 ++++++++++++++------------ ...
diff --git a/packages/cli/src/lib/ghcr.ts b/packages/cli/src/lib/ghcr.ts index 313ecced2..f556b35fc 100644 --- a/packages/cli/src/lib/ghcr.ts +++ b/packages/cli/src/lib/ghcr.ts @@ -1,50 +1,54 @@ /** * 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. ...
503- 503 | Array<{ tag_name?: string; draft?: boolean; prerelease?: boolean }>;
504- 504 versions.push(...extractReleaseVersions(data, resolved.source));
505- 505 const nextPage = getNextGitHubReleasePage(response, resolved.source);
506- 506 if (!nextPage) {
507- 507 const version = versions.sort((a, b) => compareVersions(b, a))[0];
508- 508 if (!version) {
509- 509 throw new UpgradeError(
510- 510 "network_error",
511- 511 "No version found in GitHub release"
512- 512 );
513- 513 }
514- 514 return { version, source: resolved.source };
515- 515 }
516- 516 if (visitedPages.has(nextPage)) {
517- 517 throw new UpgradeError(
518- 518 "network_error",
519- 519 "GitHub returned cyclic release pagination"
520- 520 );
521- 521 }
522- 522 visitedPages.add(nextPage);
523- 523 response = await fetchWithUpgradeError(
524- 524 ...
immutable target verified
Non-404 and malformed-metadata failures can fall back to cached code instead of failing closed.
/home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/toolkit-bridge-upgrade/packages/cli/src/commands/cli/upgrade.ts:221-235 treats every UpgradeError with reason === "network_error" as a network outage. That includes HTTP 403/429/500 from source resolution and invalid pinned-release metadata raised at /home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/toolkit-bridge-upgrade/packages/cli/src/lib/upgrade.ts:713-733. A curl installation with a cached target and patch can therefore continue after authorization, rate-limit, server, or malformed HTTP-200 responses. This bypasses the required terminal behavior and exact-metadata gate.
Fix: distinguish transport failures from HTTP and metadata failures. Permit automatic cache fallback only for typed transport failures. Keep all non-404 HTTP and malformed successful responses terminal.
Regression: pre-populate the version and patch caches, return Toolkit HTTP 403 and malformed/mismatched HTTP-200 metadata, then assert the original error propagates and no legacy probe, cache application, binary download, setup, or installation occurs.
Stable npm resolution still accepts semantic prereleases and can cross into the nightly installation path.
/home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/toolkit-bridge-upgrade/packages/cli/src/lib/upgrade.ts:570-576 accepts any non-empty npm version. Stable package-manager resolution selects this path at :661-670. A /latest response such as {"version":"1.2.3-dev.123"} reaches /home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/toolkit-bridge-upgrade/packages/cli/src/commands/cli/upgrade.ts:1075-1086, which migrates the npm installation to a Toolkit nightly binary despite the stable channel. ...