Dashboard › cli › Session 1SkBNt97Ze2f
1SkBNt97Ze2fsD8d9READ-ONLY security/supply-chain merge gate getsentry/cli PR #1569 exact base ec83887a16f780f32fba4b7d710bad262dba3a22 head f1c10a6cb1a753b8c4b8c613cd78735c3f12cba6. Inspect immutable Git objects only (git show head:path, git diff base..head), never worktree contents/state. Repo /home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/toolkit-bridge-upgrade. Inspect all changed files/tests. Verify prior blockers: exact pins bypass latest; target-driven Homebrew/npm routing; one-off pinned nightly preserves channel while artifact/changelog nightly; stable semantic prerelease exclusion latest/delta/changelog both sources; branded prefetch provenance stable source key/raw rejection; exact stable/nightly metadata; typed 404-only/fail closed; source affinity/canonical pagination/GHCR arbitrary abort reason/scope/redirect/SHA/delta/cache/offline/installer unchanged. Return non-empty findings-first PASS/CONCERN/MUST-FIX/BLOCKED exact head file:line, deterministic regression per defect, residual gaps. Final exactly MERGE or DO-NOT-MERGE. Never modify.
<skill_content name="jj-guide"> # Skill: jj-guide # jj Guide for AI Agents Jujutsu (jj) is a Git-compatible VCS with mutable commits, automatic snapshotting, no staging area, and first-class conflicts. This skill teaches you how to use it safely from a non-interactive agent environment. If `.jj/` exists in the repo root, this is a jj repo. **Use `jj` commands, not `git`.** In a colocated repo (`.jj/` *and* `.git/`), git tools can read the state, but mutations should go through `jj` so the operation log stays consistent. ## Critical Rules - **NEVER** use interactive flags (`-i`, `--interactive`). TUI prompts hang in agent environments. This applies to `jj split -i`, `jj squash -i`, `jj commit -i`, `jj resolve`, `jj diffedit`, etc. - **ALWAYS** pass `-m "msg"` when describing/committing. Without `-m`, an editor opens and hangs. - **VERIFY** mutations with `jj st` and `jj log` after `squash`, `abandon`, `rebase`, `restore`, `commit`. ...
ec83887a16f780f32fba4b7d710bad262dba3a22 f1c10a6cb1a753b8c4b8c613cd78735c3f12cba6 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 | 176 +++++-- packages/cli/src/lib/binary.ts | 154 +++++- packages/cli/src/lib/delta-upgrade.ts | 170 ++++-- packages/cli/src/lib/ghcr.ts | 102 +++- ...
...output truncated...
Full output saved to: /home/byk/.local/share/opencode/tool-output/tool_0896cb6280019J4hVNXE0yTxr4
<!-- 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/commands/cli/upgrade.ts b/packages/cli/src/commands/cli/upgrade.ts index 8d8282368..4e855a1f0 100644 --- a/packages/cli/src/commands/cli/upgrade.ts +++ b/packages/cli/src/commands/cli/upgrade.ts @@ -1,414 +1,471 @@ /** * 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..61e10568d 100644
--- a/packages/cli/src/lib/binary.ts
+++ b/packages/cli/src/lib/binary.ts
@@ -25,287 +25,429 @@ import {
} from "./custom-ca.js";
import { stringifyUnknown, UpgradeError } from "./errors.js";
import { logger } from "./logger.js";
import { isProcessRunning } from "./process-utils.js";
/** Known directories where the curl installer may place the binary */
export const KNOWN_CURL_DIRS = [".local/bin", "bin", ".sentry/bin"];
/**
* Whether the current platform's filesystem is case-insensitive by default
* (Windows, macOS). Resolved once at module load — `process.platform` never
* changes at runtime.
*/
const IS_CASE_INSENSITIVE_FS =
process.platform === "win32" || process.platform === "darwin";
/**
* Legacy install directory (relative to home) that predates the XDG layout.
* The curl installer used to drop the binary here; migration moves it out. ...
diff --git a/packages/cli/src/lib/upgrade.ts b/packages/cli/src/lib/upgrade.ts
index 945adc48d..4a23b4b25 100644
--- a/packages/cli/src/lib/upgrade.ts
+++ b/packages/cli/src/lib/upgrade.ts
@@ -1,149 +1,238 @@
/**
* 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,348 +1,441 @@
/** 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_0896d7cd1001dA5oYTRtJFWAu4
+ ) {
+ return new Response("Not Found", { status: 404 });
+ }
+ if (request.includes("getsentry/cli/releases/tags/88.88.88")) {
+ return new Response(JSON.stringify({ tag_name: "88.88.88" }), {
+ status: 200,
+ });
+ }
+ if (request.includes("getsentry/cli/releases?per_page=30")) {
+ return new Response(JSON.stringify([]), { status: 200 });
+ }
+ return new Response("Unexpected", { status: 500 });
+ });
+
+ const { context, restore } = createMockContext({ homeDir: testDir });
+ restoreStderr = restore;
+
+ await run(
+ app,
+ ["cli", "upgrade", "--check", "--method", "curl", "88.88.88"],
+ context
+ );
+
+ expect(requests).toContain(
+ ...
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
@@ -72,85 +72,85 @@ function mockFetch(
const BINARY_NAME = getPlatformBinaryName();
function versionHex(version: string): string {
return Array.from(version)
.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 ...
diff --git a/packages/cli/test/lib/release-notes.test.ts b/packages/cli/test/lib/release-notes.test.ts
index 78963b471..fb44bc47e 100644
--- a/packages/cli/test/lib/release-notes.test.ts
+++ b/packages/cli/test/lib/release-notes.test.ts
@@ -1,51 +1,57 @@
/**
* 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 ...
f1c10a6cb1a753b8c4b8c613cd78735c3f12cba6:packages/cli/src/lib/delta-upgrade.ts:88:export function isNormalizedForSource( f1c10a6cb1a753b8c4b8c613cd78735c3f12cba6:packages/cli/src/lib/delta-upgrade.ts:100:export function normalizeStableReleases( f1c10a6cb1a753b8c4b8c613cd78735c3f12cba6:packages/cli/src/lib/delta-upgrade.ts:187: const releases = normalizeStableReleases(data, source); f1c10a6cb1a753b8c4b8c613cd78735c3f12cba6:packages/cli/src/lib/delta-upgrade.ts:237:export async function fetchRecentReleases( f1c10a6cb1a753b8c4b8c613cd78735c3f12cba6:packages/cli/src/lib/delta-upgrade.ts:253: return normalizeStableReleases([], source); f1c10a6cb1a753b8c4b8c613cd78735c3f12cba6:packages/cli/src/lib/delta-upgrade.ts:258: return normalizeStableReleases([], source); f1c10a6cb1a753b8c4b8c613cd78735c3f12cba6:packages/cli/src/lib/delta-upgrade.ts:260: return normalizeStableReleases(data, source); f1c10a6cb1a753b8c4b8c613cd78735c3f12cba6:packages/cli/src/lib/delta-upgrade.ts:263: ...
diff --git a/packages/cli/test/lib/binary.test.ts b/packages/cli/test/lib/binary.test.ts
index 6fcfb3b1a..5eaa53474 100644
--- a/packages/cli/test/lib/binary.test.ts
+++ b/packages/cli/test/lib/binary.test.ts
@@ -1,93 +1,219 @@
/**
* 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,
isMusl,
...
144 resolveExistingUpgradeVersion,
201 { tag_name: "cli@99.0.0-dev.1", prerelease: false },
284 test("classifies malformed GitHub release pagination as a network error", async () => {
741 await expect(resolveExistingUpgradeVersion("1.0.0")).rejects.toThrow(
751 ])("rejects pinned Toolkit %s without legacy fallback", async (_name, body) => {
758 await expect(resolveExistingUpgradeVersion("1.0.0")).rejects.toMatchObject({
770 ])("rejects pinned nightly manifest annotation %s without legacy fallback", async (annotation) => {
794 resolveExistingUpgradeVersion("0.14.0-dev.123")
801 test("does not fall back from an explicit selected source", async () => {
1916 test("does not fall back from a non-404 Toolkit nightly failure", async () => {
1938 test("does not fall back when nightly transport error text says HTTP 404", async () => {
/usr/bin/bash: line 1: python: command not found
690 async () =>
691 new Response(JSON.stringify([{ tag_name: "cli@v3.0.0" }]), {
692 status: 200,
693 headers: { "Content-Type": "application/json" },
694 })
695 );
696
697 const version = await fetchLatestVersion("curl");
698 expect(version).toBe("3.0.0");
699 });
700 });
701
702 describe("versionExists", () => {
703 test("probes prefixed Toolkit tags and retains the selected source", async () => {
704 const requests: string[] = [];
705 mockFetch(async (url) => {
706 requests.push(String(url));
707 return new Response(JSON.stringify({ tag_name: "cli@1.0.0" }), {
708 status: 200,
709 });
710 });
711
712 await expect(versionExists("curl", "1.0.0")).resolves.toBe(true);
713 expect(requests).toEqual([
714 "https://api.github.com/repos/getsentry/toolkit/releases/tags/cli%401.0.0",
715 ]);
716 ...
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 ...
diff --git a/packages/cli/test/commands/cli/upgrade.test.ts b/packages/cli/test/commands/cli/upgrade.test.ts
index ca9688e46..b67847fb7 100644
--- a/packages/cli/test/commands/cli/upgrade.test.ts
+++ b/packages/cli/test/commands/cli/upgrade.test.ts
@@ -160,30 +160,34 @@ function createMockContext(
} else {
process.env.SENTRY_PLAIN_OUTPUT = origPlain;
}
},
};
}
/**
* Mock fetch to simulate GHCR manifest returning a specific nightly version.
* Handles token exchange and manifest fetch.
*/
function mockGhcrNightlyVersion(version: string): void {
mockFetch(async (url) => {
const urlStr = String(url);
+ if (urlStr === "https://api.github.com/repos/getsentry/toolkit") {
+ return new Response(null, { status: 200 });
+ }
+
// GHCR anonymous token exchange
if (urlStr.includes("ghcr.io/token")) {
return new Response(JSON.stringify({ token: "test-token" }), {
status: 200,
headers: { "content-type": ...
--- packages/cli/src/commands/cli/upgrade.ts ---
220 };
221 } catch (error) {
222 // Automatic offline fallback: only for curl-installed binaries (package
223 // managers need the network for the actual install, not just version
224 // discovery), and only for network errors (not version_not_found etc.)
225 if (
226 method !== "curl" ||
227 !(error instanceof UpgradeError && error.reason === "network_error")
228 ) {
229 throw error;
230 }
231 try {
232 const target = resolveOfflineTarget(versionArg);
233 log.warn("Network unavailable, falling back to cached upgrade target");
234 log.info(`Using cached target: ${target}`);
235 return { kind: "target", target, offline: "network-fallback" };
236 } catch {
237 // No cached version either — re-throw original network error
238 throw error;
239 }
240 }
241 }
242
243 /**
...
128 for (let attempt = 0; attempt <= GHCR_MAX_RETRIES; attempt++) {
129 try {
130 const response = await customFetch(url, {
131 ...init,
132 signal: buildSignal(timeout, externalSignal),
133 });
134 return response;
135 } catch (error) {
136 lastError = error instanceof Error ? error : new Error(String(error));
137 // Propagate external abort immediately — don't retry caller cancellation
138 if (isExternalAbort(lastError, externalSignal)) {
139 throw lastError;
140 }
141 // Only retry on timeout or network errors — not HTTP errors
142 if (attempt >= GHCR_MAX_RETRIES || !isRetryableError(lastError)) {
143 break;
144 }
145 }
146 }
147
148 throw new UpgradeError(
149 "network_error",
150 `${context}: ${lastError?.message ?? ...
packages/cli/src/lib/db/install-info.ts packages/cli/test/lib/db/install-info.test.ts packages/cli/test/lib/install-script.test.ts
apps/cli-docs/public/install apps/cli-docs/src/components/InstallSelector.astro packages/cli/install packages/cli/src/commands/cli/uninstall.ts packages/cli/src/lib/db/install-info.ts packages/cli/test/commands/cli/uninstall.test.ts packages/cli/test/lib/db/install-info.test.ts packages/cli/test/lib/install-script.test.ts
apps/cli-docs/public/install c6850ca5ea9254ff1b25a1850d31de63cec527f6 apps/cli-docs/public/install c6850ca5ea9254ff1b25a1850d31de63cec527f6 packages/cli/install e05cef2018d3fb6ddde9dcc1a4d7c47c0db3b3da packages/cli/install e05cef2018d3fb6ddde9dcc1a4d7c47c0db3b3da packages/cli/test/lib/install-script.test.ts b1f22bbab8c434e110b1fc59525dda160cccc074 packages/cli/test/lib/install-script.test.ts b1f22bbab8c434e110b1fc59525dda160cccc074
f1c10a6cb1a753b8c4b8c613cd78735c3f12cba6:packages/cli/src/lib/delta-upgrade.ts:21: makeCache,
f1c10a6cb1a753b8c4b8c613cd78735c3f12cba6:packages/cli/src/lib/patch-cache.ts:2:import { makeCache, type PatchCache, type PatchChain } from "binpatch";
f1c10a6cb1a753b8c4b8c613cd78735c3f12cba6:packages/cli/src/lib/patch-cache.ts:10: return makeCache(join(getConfigDir(), "patch-cache"));
f1c10a6cb1a753b8c4b8c613cd78735c3f12cba6:packages/cli/src/lib/upgrade.ts:66:import { clearPatchCache } from "./patch-cache.js";
f1c10a6cb1a753b8c4b8c613cd78735c3f12cba6:packages/cli/src/lib/version-check.ts:30:import { cleanupPatchCache } from "./patch-cache.js";
f1c10a6cb1a753b8c4b8c613cd78735c3f12cba6:packages/cli/test/lib/bspatch.test.ts:523: `bspatch-cache-${Date.now()}-${Math.random().toString(36).slice(2)}-${name}`
f1c10a6cb1a753b8c4b8c613cd78735c3f12cba6:packages/cli/test/lib/patch-cache.test.ts:20:} from ...
f1c10a6cb1a753b8c4b8c613cd78735c3f12cba6:packages/cli/src/lib/binary.ts:490:export async function fetchWithUpgradeError(
775 }
776
777 async function standaloneVersionExists(
778 version: string,
779 source?: UpgradeSource
780 ): Promise<boolean> {
781 if (source) {
782 if (isNightlyVersion(version)) {
783 return nightlyVersionExists(version, source);
784 }
785 const response = await fetchWithUpgradeError(
786 getGitHubReleaseByTagUrl(version, source),
787 { headers: getGitHubHeaders() },
788 "GitHub"
789 );
790 if (response.ok) {
791 return true;
792 }
793 if (response.status === 404) {
794 return false;
795 }
796 throw new UpgradeError(
797 "network_error",
798 `Failed to fetch from GitHub: HTTP ${response.status}`
799 );
800 }
801 const resolved = await resolveExistingUpgradeVersion(version);
802 ...
460 if (existsSync(dir) && pathDirs.some((p) => samePath(p, dir))) {
461 return dir;
462 }
463 }
464
465 // 5. XDG-aligned fallback — setup will handle adding this to PATH
466 return join(homeDir, ".local", "bin");
467 }
468
469 /**
470 * Build headers for GitHub API requests.
471 */
472 export function getGitHubHeaders(): Record<string, string> {
473 return {
474 Accept: "application/vnd.github.v3+json",
475 "User-Agent": getUserAgent(),
476 };
477 }
478
479 /**
480 * Fetch wrapper that converts network errors to UpgradeError.
481 * Handles DNS failures, timeouts, and other connection issues.
482 *
483 * @param url - URL to fetch
484 * @param init - Fetch options
485 * @param serviceName - Service name for error messages (e.g., "GitHub")
486 * @returns Response object
487 * @throws {UpgradeError} On network failure
488 * @throws {Error} AbortError if ...
ec83887a16f780f32fba4b7d710bad262dba3a22 f04818a2473a139b9ba1a0a0c94bb9fb80e9cc23788d06cf666c1deabd365f6e -
diff --git a/packages/cli/test/lib/upgrade.test.ts b/packages/cli/test/lib/upgrade.test.ts
index 7c6f63a4b..6cfc7fae9 100644
--- a/packages/cli/test/lib/upgrade.test.ts
+++ b/packages/cli/test/lib/upgrade.test.ts
@@ -120,6 +120,7 @@ import {
getBinaryDownloadUrl,
isNightlyVersion,
releaseLock,
+ UPGRADE_SOURCES,
} from "../../src/lib/binary.js";
import {
clearInstallInfo,
@@ -140,6 +141,7 @@ const {
fetchLatestVersion,
getCurlInstallPaths,
parseInstallationMethod,
+ resolveExistingUpgradeVersion,
startCleanupOldBinary,
versionExists,
} = await import("../../src/lib/upgrade.js");
@@ -188,18 +190,175 @@ describe("parseInstallationMethod", () => {
});
describe("fetchLatestFromGitHub", () => {
- test("returns version from GitHub API", async () => {
- mockFetch(
- async () =>
- new Response(
- JSON.stringify({
- tag_name: "v1.2.3",
- }),
+ test("selects the latest CLI-prefixed Toolkit release", async () => ...
381 prefetchedReleases: releases,
390 test("normalizes raw prefetched Toolkit releases without fetching", async () => {
402 prefetchedReleases: [
421 test("rejects unprefixed raw prefetched releases for Toolkit", async () => {
427 prefetchedReleases: [
441 prefetchedReleases: [
441 test("preserves an arbitrary external cancellation reason", async () => {
447 controller.abort(reason);
636 setReleaseChannel("nightly");
654 setReleaseChannel("nightly");
692 setReleaseChannel("nightly");
1259 setReleaseChannel("nightly");
1275 test("validates an npm stable pin through npm while tracking nightly", async () => {
1281 setReleaseChannel("nightly");
1292 expect(requests).toContain("https://registry.npmjs.org/sentry/1.2.3");
1000 log.debug(`Installation method: ${method}`);
1001 log.debug(`Current version: ${CLI_VERSION}`);
1002
1003 const resolved = await withProgress(
1004 { message: "Checking for updates...", json: flags.json },
1005 async () =>
1006 resolveTargetWithFallback({
1007 resolveOpts: { method, channel, versionArg, channelChanged, flags },
1008 versionArg,
1009 offline: flags.offline,
1010 method,
1011 persistChannelFn: () =>
1012 persistChannel(channel, channelChanged, version),
1013 })
1014 );
1015 // Early exit for check-only (online) and up-to-date results.
1016 if (resolved.kind === "done") {
1017 const result = resolved.result;
1018 // For --check with a version diff, fetch changelog before returning.
1019 if (
1020 result.action === "checked" &&
1021 result.currentVersion !== ...
Raw prefetched releases bypass provenance enforcement
/home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/toolkit-bridge-upgrade/packages/cli/src/lib/release-notes.ts:430-438,640-642,746
prefetchedReleases remains an unbranded GitHubRelease[]. When the source brand is absent or mismatched, normalizeChangelogReleases() reinterprets the raw array under the caller-provided source instead of rejecting it. This defeats the source key added in delta-upgrade.ts:75-120 and leaves release provenance ambiguous. The test at packages/cli/test/lib/release-notes.test.ts:390-419 explicitly preserves the unwanted raw-acceptance behavior.
Fix: accept NormalizedGitHubReleases and require isNormalizedForSource() before reuse. Return null or fetch from the selected source when the brand is absent or mismatched; never normalize caller-supplied raw prefetched data.
Regression: pass (a) raw valid Toolkit releases, (b) Toolkit-normalized releases with the legacy source, and (c) legacy-normalized releases with Toolkit. All three must be rejected without fetching or producing a changelog. Same-source branded releases must succeed.
A pinned stable target inherits the persisted nightly changelog channel
/home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/toolkit-bridge-upgrade/packages/cli/src/commands/cli/upgrade.ts:276-280,1023-1029,1037-1043
getArtifactChannel() returns trackingChannel for every non-nightly target. Therefore, while tracking nightly, sentry cli upgrade 1.2.3 correctly routes installation through npm or stable GitHub assets but incorrectly requests a nightly changelog. With a stable target and nightly current version, timestamp parsing normally suppresses the changelog entirely. Artifact behavior must always follow the target; only persisted channel state follows the tracking channel. ...