Dashboard › cli › Session 0954cCjZBZct
0954cCjZBZctGde9VPerform a READ-ONLY independent security/supply-chain merge-gate review of getsentry/cli PR #1569. Repository: /home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/toolkit-bridge-upgrade. Exact immutable base: ec83887a16f780f32fba4b7d710bad262dba3a22. Exact immutable head: 9525f72c70fabbb23ec21d13cf86add58f6287cc. Use immutable Git objects only for evidence; never modify files, commits, branches, worktree, or VCS state. Inspect every changed source/test file; verify merge base, trees, paths, complete diff and git diff --check. Adversarially inspect: typed genuine-404-only Toolkit-to-legacy fallback; terminal non-404/transport/TLS/malformed responses; source affinity across all GitHub/GHCR/delta/prefetch/changelog/download/setup stages; trusted pagination reconstruction/cycle handling; caller abort identity and body transport versus malformed JSON; strict npm shape/status across npm/pnpm/bun/yarn and no subprocess on invalid/non-404 responses; canonical Toolkit and legacy tags; stable/nightly pin and release metadata validation; GHCR trimmed token, non-array annotations, OCI manifest/layer/digest/size/tag schemas, repeated cursors; bearer token removal on redirects; target/tracking channel separation; offline/cache behavior; delta final SHA/cleanup; end-to-end nightly selected-source blob/setup assertion. Settled contract: source-neutral patch cache is intentional under mandatory byte-identical dual publication plus final SHA verification. Raise it only with a proven issue surviving both guarantees. Return substantive non-empty findings first, ordered by severity, with PASS/CONCERN/MUST-FIX/BLOCKED and exact HEAD file:line evidence. Every defect requires deterministic regression and concrete fix. State scope and residual risks. If blocked, include exact command/error. End the final line exactly MERGE or DO-NOT-MERGE.
No files found
ec83887a16f780f32fba4b7d710bad262dba3a22 base_commit=ec83887a16f780f32fba4b7d710bad262dba3a22 base_tree=110d6530e57ca4fbfb2a3f98e52013d2668b5b5e base_parents=e7beceb6ba09b64dc277ae1732df04e65500062b base_subject=fix(event): Prevent immediate retry of rate-limited org in event view fallback (#1566) head_commit=9525f72c70fabbb23ec21d13cf86add58f6287cc head_tree=22f49363a13e32c36dcd9e1ca95916809c2dc271 head_parents=a84012184c79c2566c7466aa6beda5f767199218 head_subject=fix(cli): harden upgrade metadata validation
(no output)
M packages/cli/src/commands/cli/upgrade.ts M packages/cli/src/lib/binary.ts M packages/cli/src/lib/delta-upgrade.ts M packages/cli/src/lib/errors.ts M packages/cli/src/lib/ghcr.ts M packages/cli/src/lib/release-notes.ts M packages/cli/src/lib/upgrade.ts M packages/cli/src/lib/version-check.ts M packages/cli/test/commands/cli.test.ts M packages/cli/test/commands/cli/upgrade.test.ts M packages/cli/test/lib/binary.test.ts M packages/cli/test/lib/delta-upgrade.mocked.test.ts M packages/cli/test/lib/delta-upgrade.test.ts M packages/cli/test/lib/ghcr.test.ts M packages/cli/test/lib/release-notes.test.ts M packages/cli/test/lib/upgrade.test.ts M packages/cli/test/lib/version-check.test.ts packages/cli/src/commands/cli/upgrade.ts | 180 +++-- packages/cli/src/lib/binary.ts | 191 +++++- packages/cli/src/lib/delta-upgrade.ts | 170 ++++- packages/cli/src/lib/errors.ts | 8 + packages/cli/src/lib/ghcr.ts | ...
commit 04867eaf9b412a62d4b1ea1c59c2b382c557bd0b 04867eaf9b412a62d4b1ea1c59c2b382c557bd0b cc356b77799633918710e0c8d358034894f86147 ec83887a16f780f32fba4b7d710bad262dba3a22 feat(cli): add toolkit upgrade bridge commit 9c874298e3b57515ba36d35140627a668a46dc7b 9c874298e3b57515ba36d35140627a668a46dc7b 4dd2c6153592d15804b1aaef8da9015e0df0345b 04867eaf9b412a62d4b1ea1c59c2b382c557bd0b fix(cli): address toolkit bridge review commit 723375b6ce4e37884e760fa0f848e7e7e3effcc9 723375b6ce4e37884e760fa0f848e7e7e3effcc9 c0c0ace97578535769b33929fb2494113eec3e5d 9c874298e3b57515ba36d35140627a668a46dc7b fix(cli): address upgrade review findings commit dbee7e9f2a4555e044f74dfebad002cccfa43a1f dbee7e9f2a4555e044f74dfebad002cccfa43a1f 024dbc7afa15d0632eb4c15ed6c808dab974a9c5 723375b6ce4e37884e760fa0f848e7e7e3effcc9 test(cli): update toolkit upgrade fixtures commit 5ffa5cd0fe02c0a5dc5bec9a8a099f4135c0b447 5ffa5cd0fe02c0a5dc5bec9a8a099f4135c0b447 a1cbad382fe2fec2c40f31bd64eaf1898b779428 ...
fatal: :(glob)**/AGENTS.md: pathspec magic not supported by this command: 'glob'
--- AGENTS.md --- # 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 ...
1050 1114 packages/cli/src/commands/cli/upgrade.ts 597 768 packages/cli/src/lib/binary.ts 627 725 packages/cli/src/lib/delta-upgrade.ts 1004 1012 packages/cli/src/lib/errors.ts 520 659 packages/cli/src/lib/ghcr.ts 739 793 packages/cli/src/lib/release-notes.ts 1144 1502 packages/cli/src/lib/upgrade.ts 496 501 packages/cli/src/lib/version-check.ts 236 230 packages/cli/test/commands/cli.test.ts 1202 1584 packages/cli/test/commands/cli/upgrade.test.ts 790 974 packages/cli/test/lib/binary.test.ts 500 500 packages/cli/test/lib/delta-upgrade.mocked.test.ts 1846 2034 packages/cli/test/lib/delta-upgrade.test.ts 558 773 packages/cli/test/lib/ghcr.test.ts 295 512 packages/cli/test/lib/release-notes.test.ts 2153 2819 packages/cli/test/lib/upgrade.test.ts 547 670 packages/cli/test/lib/version-check.test.ts
diff --git a/packages/cli/src/commands/cli/upgrade.ts b/packages/cli/src/commands/cli/upgrade.ts index 8d8282368..b3039d86b 100644 --- a/packages/cli/src/commands/cli/upgrade.ts +++ b/packages/cli/src/commands/cli/upgrade.ts @@ -1,86 +1,90 @@ /** * sentry cli upgrade * * Self-update the Sentry CLI to the latest or a specific version. * After upgrading, spawns the NEW binary with `cli setup` to update * completions, agent skills, and record installation metadata. * * Supports two release channels: * - stable (default): tracks the latest GitHub release * - nightly: tracks the rolling nightly prerelease built from main * * The channel can be set via --channel or by passing "nightly"/"stable" * as the version argument. The choice is persisted in the local database * so that subsequent bare `sentry cli upgrade` calls use the same channel. ...
diff --git a/packages/cli/src/lib/binary.ts b/packages/cli/src/lib/binary.ts
index 75582612a..e64f5059e 100644
--- a/packages/cli/src/lib/binary.ts
+++ b/packages/cli/src/lib/binary.ts
@@ -1,56 +1,60 @@
/**
* 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
@@ -3,205 +3,291 @@
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,
PATCH_TAG_PREFIX,
type PatchCache,
type PatchChain,
type ...
diff --git a/packages/cli/src/lib/errors.ts b/packages/cli/src/lib/errors.ts
index f81b8c70c..b21505cd1 100644
--- a/packages/cli/src/lib/errors.ts
+++ b/packages/cli/src/lib/errors.ts
@@ -591,60 +591,68 @@ export type UpgradeErrorReason =
| "version_not_found"
| "offline_cache_miss";
/**
* Upgrade-related errors.
*
* @param reason - Type of upgrade failure
* @param message - Custom message (uses default if not provided)
*/
export class UpgradeError extends CliError {
readonly reason: UpgradeErrorReason;
constructor(reason: UpgradeErrorReason, message?: string) {
const defaultMessages: Record<UpgradeErrorReason, string> = {
unknown_method:
"Could not detect installation method. Use --method to specify.",
unsupported_operation:
"This operation is not supported for this installation method.",
network_error: "Failed to fetch version information.",
execution_failed: "Upgrade command failed.",
...
diff --git a/packages/cli/src/lib/ghcr.ts b/packages/cli/src/lib/ghcr.ts index 43d6deb7e..5bca7c061 100644 --- a/packages/cli/src/lib/ghcr.ts +++ b/packages/cli/src/lib/ghcr.ts @@ -1,520 +1,659 @@ /** * GHCR (GitHub Container Registry) Client * * Encapsulates the OCI download protocol for fetching nightly CLI binaries * from ghcr.io/getsentry/cli. Nightly builds are pushed as OCI artifacts * via ORAS with the version baked into the manifest annotation. * * Key design decisions: * - Anonymous access: nightly package is public; no token needed beyond the * standard ghcr.io anonymous token exchange. * - Version discovery from manifest annotation: `annotations.version` in the * OCI manifest holds the nightly version. Checking the latest version only * requires a token exchange + manifest fetch (2 HTTP requests total). * - Redirect quirk: ghcr.io blob downloads return 307 to Azure Blob Storage. ...
diff --git a/packages/cli/src/lib/release-notes.ts b/packages/cli/src/lib/release-notes.ts index ddf617535..903ca51f6 100644 --- a/packages/cli/src/lib/release-notes.ts +++ b/packages/cli/src/lib/release-notes.ts @@ -1,52 +1,59 @@ /** * Release Notes Parser & Aggregation * * Extracts user-facing changelog entries from GitHub Release bodies (stable) * or conventional commit messages (nightly). Uses `marked.lexer()` for * AST-based section extraction and produces structured data that can be * re-serialized as filtered markdown for rendering via `renderMarkdown()`. ...
diff --git a/packages/cli/src/lib/upgrade.ts b/packages/cli/src/lib/upgrade.ts
index 945adc48d..b29d67ff2 100644
--- a/packages/cli/src/lib/upgrade.ts
+++ b/packages/cli/src/lib/upgrade.ts
@@ -4,106 +4,204 @@
* 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,
determineInstallDir,
...
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,
...
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
@@ -115,122 +115,116 @@ describe("upgradeCommand.func", () => {
restoreStderr?.();
restoreStderr = undefined;
globalThis.fetch = originalFetch;
});
// 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": ...
diff --git a/packages/cli/test/commands/cli/upgrade.test.ts b/packages/cli/test/commands/cli/upgrade.test.ts
index ca9688e46..b7fead58f 100644
--- a/packages/cli/test/commands/cli/upgrade.test.ts
+++ b/packages/cli/test/commands/cli/upgrade.test.ts
@@ -35,16 +35,17 @@ import { CLI_VERSION } from "../../../src/lib/constants.js";
import {
clearInstallInfo,
setInstallInfo,
} 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;
/** Helper to mock fetch */
function mockFetch(
fn: (url: string | URL | Request, init?: RequestInit) => Promise<Response>
@@ -167,16 +168,20 @@ function createMockContext(
/**
* Mock fetch to simulate GHCR manifest ...
diff --git a/packages/cli/test/lib/binary.test.ts b/packages/cli/test/lib/binary.test.ts
index 6fcfb3b1a..29ead3828 100644
--- a/packages/cli/test/lib/binary.test.ts
+++ b/packages/cli/test/lib/binary.test.ts
@@ -18,56 +18,183 @@ 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,
+ parseUpgradeJson,
releaseLock,
replaceBinarySync,
+ resolveUpgradeSource,
samePath,
+ UPGRADE_SOURCES,
+ UpgradeSourceNotFoundError,
} from "../../src/lib/binary.js";
import { UpgradeError } from "../../src/lib/errors.js";
describe("getBinaryDownloadUrl", () => {
test("builds ...
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
@@ -92,45 +92,45 @@ function tempFile(name: string): string {
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 = 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 = ...
diff --git a/packages/cli/test/lib/delta-upgrade.test.ts b/packages/cli/test/lib/delta-upgrade.test.ts
index 3752d4ae6..68d1cfbdf 100644
--- a/packages/cli/test/lib/delta-upgrade.test.ts
+++ b/packages/cli/test/lib/delta-upgrade.test.ts
@@ -6,18 +6,21 @@
* async orchestration functions tested via fetch mocking.
*/
import { createHash } from "node:crypto";
import { existsSync, unlinkSync } from "node:fs";
import { access, readFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
-import { afterEach, beforeEach, describe, expect, test } from "vitest";
-import { getPlatformBinaryName } from "../../src/lib/binary.js";
+import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
+import {
+ getPlatformBinaryName,
+ UPGRADE_SOURCES,
+} from "../../src/lib/binary.js";
import {
applyPatchChain,
attemptDeltaUpgrade,
canAttemptDelta,
downloadStablePatch,
type ExtractStableChainOpts,
extractSha256,
...
diff --git a/packages/cli/test/lib/ghcr.test.ts b/packages/cli/test/lib/ghcr.test.ts
index e5c81d532..a55612b5a 100644
--- a/packages/cli/test/lib/ghcr.test.ts
+++ b/packages/cli/test/lib/ghcr.test.ts
@@ -1,25 +1,27 @@
/**
* GHCR Client Tests
*
* Unit tests for the GHCR/OCI download protocol helpers.
* All HTTP calls are mocked via globalThis.fetch to avoid network access.
*/
import { afterEach, beforeEach, describe, expect, test } from "vitest";
+import { UPGRADE_SOURCES } from "../../src/lib/binary.js";
import { UpgradeError } from "../../src/lib/errors.js";
import {
downloadLayerBlob,
downloadNightlyBlob,
fetchManifest,
fetchNightlyManifest,
findLayerByFilename,
GHCR_REPO,
GHCR_TAG,
+ GhcrManifestHttpError,
getAnonymousToken,
getNightlyVersion,
listTags,
type OciManifest,
} from "../../src/lib/ghcr.js";
/** Store original fetch for restoration */
let originalFetch: typeof globalThis.fetch;
@@ -32,31 +34,31 @@ function ...
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,
...
diff --git a/packages/cli/test/lib/upgrade.test.ts b/packages/cli/test/lib/upgrade.test.ts
index 7c6f63a4b..d10f21dd6 100644
--- a/packages/cli/test/lib/upgrade.test.ts
+++ b/packages/cli/test/lib/upgrade.test.ts
@@ -117,12 +117,13 @@ vi.mock("node:child_process", async (importOriginal) => {
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";
@@ -137,12 +138,13 @@ const {
fetchLatestFromGitHub,
fetchLatestFromNpm,
fetchLatestNightlyVersion,
fetchLatestVersion,
getCurlInstallPaths,
parseInstallationMethod,
+ resolveExistingUpgradeVersion,
startCleanupOldBinary,
versionExists,
} = await import("../../src/lib/upgrade.js");
import { TEST_TMP_DIR, ...
Your security review returned EMPTY and cannot satisfy the merge gate. Resume the SAME immutable review now and return a substantive non-empty report. Exact base: ec83887a16f780f32fba4b7d710bad262dba3a22. Exact head: 9525f72c70fabbb23ec21d13cf86add58f6287cc. Repository: /home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/toolkit-bridge-upgrade. Use only immutable Git objects; never inspect/edit worktree files or mutate VCS. If any access or command fails, report BLOCKED first with the exact command/error and end DO-NOT-MERGE. Otherwise state merge base, base/head tree SHAs, changed paths inspected, patch hash or equivalent immutable verification, and git diff --check result. Inspect every changed source and test object. Report security findings first with exact HEAD file:line evidence, classified MUST-FIX, CONCERN, or PASS. Recheck 404-only fallback, source affinity, trusted pagination, cancellation and response-body error typing, npm/GHCR runtime validation, package-manager no-spawn failures, tag and pinned-version validation, GHCR cursor and redirect authorization boundaries, channel/offline behavior, and delta final SHA/cleanup. Source-neutral patch caching is settled because dual-published artifacts must be byte-identical and final output SHA-256 is verified; raise it only with a proven issue surviving both guarantees. Every defect needs a deterministic regression and concrete fix. State residual risks even if no defects. Empty output is forbidden. Final line exactly MERGE or DO-NOT-MERGE.
9525f72c70fabbb23ec21d13cf86add58f6287cc:packages/cli/src/commands/cli/upgrade.ts:59: resolveExistingUpgradeVersion,
9525f72c70fabbb23ec21d13cf86add58f6287cc:packages/cli/src/commands/cli/upgrade.ts:60: resolveLatestUpgradeVersion,
9525f72c70fabbb23ec21d13cf86add58f6287cc:packages/cli/src/commands/cli/upgrade.ts:301: if (!(await versionExists(lookupMethod, target))) {
9525f72c70fabbb23ec21d13cf86add58f6287cc:packages/cli/src/commands/cli/upgrade.ts:309: const resolved = await resolveExistingUpgradeVersion(target);
9525f72c70fabbb23ec21d13cf86add58f6287cc:packages/cli/src/commands/cli/upgrade.ts:341: ? await resolveLatestUpgradeVersion(channel)
9525f72c70fabbb23ec21d13cf86add58f6287cc:packages/cli/src/commands/cli/upgrade.ts:695: executeUpgrade(method, target, downloadTag, offline, setMessage, source)
9525f72c70fabbb23ec21d13cf86add58f6287cc:packages/cli/src/commands/cli/upgrade.ts:783: executeUpgrade("curl", target, downloadTag, undefined, setMessage, ...
130
131 return {
132 channel: getReleaseChannel(),
133 versionArg: positional,
134 };
135 }
136
137 /**
138 * Resolve the target version from the local cache (SQLite) instead of
139 * fetching from the network. Used by `--offline` and as automatic
140 * fallback when `fetchLatestVersion()` hits a network error.
141 *
142 * @param versionArg - Explicit version from the user, bypasses the cache lookup
143 * @returns The target version string
144 * @throws {UpgradeError} When no cached version is available
145 */
146 function resolveOfflineTarget(versionArg: string | undefined): string {
147 if (versionArg) {
148 return versionArg.replace(VERSION_PREFIX_REGEX, "");
149 }
150 const { latestVersion } = getVersionCheckInfo();
151 if (!latestVersion) {
152 throw new UpgradeError(
153 "network_error",
154 "No cached version available. ...
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";
...
370 // Skip if already on target — unless forced or switching channels
371 if (CLI_VERSION === resolvedTarget && !flags.force && !channelChanged) {
372 return {
373 kind: "done",
374 result: {
375 action: "up-to-date",
376 currentVersion: CLI_VERSION,
377 targetVersion: resolvedTarget,
378 channel,
379 method,
380 forced: false,
381 },
382 };
383 }
384
385 return { kind: "target", target: resolvedTarget, source };
386 }
387
388 /**
389 * Build the structured result for check-only mode. ...
650 ) {
651 return xdgInstallDir;
652 }
653 return currentInstallDir;
654 }
655
656 /**
657 * Execute the standard upgrade path: download via curl or package manager,
658 * then run setup on the new binary.
659 */
660 async function executeStandardUpgrade(opts: {
661 method: InstallationMethod;
662 channel: ReleaseChannel;
663 versionArg: string | undefined;
664 target: string;
665 execPath: string;
666 entryPath?: string;
667 pathEnv?: string;
668 offline?: OfflineMode;
669 json?: boolean;
670 noAgentSkills: boolean;
671 source?: UpgradeSource;
672 }): Promise<void> {
673 const {
674 method,
675 channel,
676 versionArg,
677 target,
678 execPath,
679 entryPath,
680 pathEnv,
681 offline,
682 json,
683 noAgentSkills,
684 source,
685 } = opts;
686
687 // Use the rolling "nightly" tag only ...
930 " stable (default) Latest stable release\n" + 931 " nightly Built from main, updated on every commit\n\n" + 932 "The channel is persisted so that subsequent bare `sentry cli upgrade` calls\n" + 933 "use the same channel.\n\n" + 934 "Examples:\n" + 935 " sentry cli upgrade # Update to latest (using persisted channel)\n" + 936 " sentry cli upgrade nightly # Switch to nightly channel and update\n" + 937 " sentry cli upgrade stable # Switch back to stable channel and update\n" + 938 " sentry cli upgrade 0.5.0 # Install a specific stable version\n" + 939 " sentry cli upgrade --check # Check for updates without installing\n" + 940 " sentry cli upgrade --force # Force re-download even if up to date\n" + 941 " sentry cli upgrade --method npm # Force using npm to upgrade\n" + 942 " sentry cli upgrade --offline ...
90 }
91
92 /**
93 * How the CLI was installed. Determines the upgrade strategy.
94 *
95 * Defined here (alongside other installation constants like
96 * {@link KNOWN_CURL_DIRS}) so that both `upgrade.ts` and
97 * `db/install-info.ts` can import it without creating a circular
98 * dependency.
99 */
100 export type InstallationMethod =
101 | "curl"
102 | "brew"
103 | "npm"
104 | "pnpm"
105 | "bun"
106 | "yarn"
107 | "unknown";
108
109 /** A repository pair that hosts CLI stable releases and nightly OCI images. */
110 export type UpgradeSource = {
111 /** GitHub `owner/repository` containing CLI release assets. */
112 readonly githubRepo: string;
113 /** GHCR `owner/package` containing CLI nightly images and delta patches. */
114 readonly ghcrRepo: string;
115 /** Prefix attached to CLI release tags in this repository. ...
450 if (env.SENTRY_INSTALL_DIR) {
451 return env.SENTRY_INSTALL_DIR;
452 }
453
454 // 2. XDG_BIN_HOME override — honored only when absolute, per the XDG spec
455 const xdgBinHome = env.XDG_BIN_HOME;
456 if (xdgBinHome && isAbsolute(xdgBinHome)) {
457 return xdgBinHome;
458 }
459
460 // 3-4. Check well-known directories that are already in PATH. samePath keeps
461 // the membership check case-insensitive on Windows/macOS, where a PATH entry
462 // can differ in casing from the computed directory yet be the same dir.
463 const candidates = [join(homeDir, ".local", "bin"), join(homeDir, "bin")];
464
465 for (const dir of candidates) {
466 if (existsSync(dir) && pathDirs.some((p) => samePath(p, dir))) {
467 return dir;
468 }
469 }
470
471 // 5. XDG-aligned fallback — setup will handle adding this to PATH
472 return join(homeDir, ".local", "bin");
473 }
474
...
470 // Auto-save detected method for future runs (best-effort —
471 // a read-only or broken DB shouldn't block detection)
472 if (legacyMethod !== "unknown") {
473 try {
474 setInstallInfo({
475 method: legacyMethod,
476 path: process.execPath,
477 version: CLI_VERSION,
478 });
479 } catch {
480 log.debug("Failed to persist install info (DB may be read-only)");
481 }
482 }
483
484 return legacyMethod;
485 }
486
487 // Version Fetching
488
489 /**
490 * Fetch the latest version from GitHub releases.
491 *
492 * @param signal - Optional AbortSignal to cancel the request
493 * @returns Latest version string (without 'v' prefix)
494 * @throws {UpgradeError} When fetch fails or response is invalid
495 * @throws {Error} AbortError if signal is aborted
496 */
497 export async function fetchLatestFromGitHubWithSource(
498 signal?: ...
1320 * Returns null silently on any failure so the caller can fall back.
1321 *
1322 * @param version - Target version to upgrade to
1323 * @param destPath - Path to write the patched binary
1324 * @returns Delta result with SHA-256 and size info, or null if delta is unavailable
1325 */
1326 // biome-ignore lint/nursery/useMaxParams: mirrors the established download helper while forwarding source affinity.
1327 async function tryDeltaUpgrade(
1328 version: string,
1329 destPath: string,
1330 offline?: boolean,
1331 setMessage?: SetMessage,
1332 source: UpgradeSource = PRIMARY_UPGRADE_SOURCE
1333 ): Promise<DeltaResult | null> {
1334 return await attemptDeltaUpgrade(
1335 version,
1336 process.execPath,
1337 destPath,
1338 offline,
1339 setMessage,
1340 source
1341 );
1342 }
1343
1344 /**
1345 * Download the full binary (non-delta path). ...
900
901 /** Result from downloadBinaryToTemp — includes both the binary path and lock path */
902 export type DownloadResult = {
903 /** Path to the downloaded temporary binary */
904 tempBinaryPath: string;
905 /** Path to the lock file held during download (caller must release after child exits) */
906 lockPath: string;
907 /** Size of delta patch in bytes, when delta upgrade was used instead of full download */
908 patchBytes?: number;
909 };
910
911 /**
912 * Write `chunk` to `fd` in full, looping to handle short writes
913 * (interrupted syscalls, partial writes under memory pressure).
914 *
915 * Throws on a 0-byte return — that's the kernel telling us the fd is
916 * unwritable and no further progress is possible, so we surface it
917 * rather than spin forever.
918 */
919 function writeChunkSync(fd: number, chunk: Uint8Array): void {
920 let written = 0;
921 while (written < ...
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. ...
390
391 /**
392 * Find the layer matching a given filename in an OCI manifest.
393 *
394 * ORAS sets `org.opencontainers.image.title` to the filename for each pushed
395 * file. This function searches layers for the matching title annotation.
396 *
397 * @param manifest - OCI manifest containing layers
398 * @param filename - Filename to find (e.g., "sentry-linux-x64.gz")
399 * @returns Matching layer
400 * @throws {UpgradeError} When no layer matches the filename
401 */
402 export function findLayerByFilename(
403 manifest: OciManifest,
404 filename: string
405 ): OciLayer {
406 const layer = manifest.layers.find(
407 (l) => l.annotations?.["org.opencontainers.image.title"] === filename
408 );
409 if (!layer) {
410 throw new UpgradeError(
411 "version_not_found",
412 `No nightly build found for ${filename}`
413 );
414 }
415 return layer;
416 }
417
...
* 🔴 (11:39) User stated package managers always need network access to fetch and install packages; offline upgrade remains supported only for curl-installed binaries. * 🔴 (11:39) User stated nightly builds are GitHub-only, so nightly version lookup must always use the curl (GitHub) path regardless of the current installation method. * 🟡 (11:39) Diff in `packages/cli/src/commands/cli/upgrade.ts…
Date: Sep 10, 2026 * 🔴 [requested-review] (11:35) User requested a READ-ONLY independent security/supply-chain merge-gate review of getsentry/cli PR #1569 in `/home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/toolkit-bridge-upgrade`. * 🔴 [enforced-workflow] (11:35) User required immutable Git objects as the sole evidence source and stated never to modify files, c…