Dashboard › cli › Session 0rGsfNZYXX3H
0rGsfNZYXX3HLfib6READ-ONLY final security/supply-chain gate getsentry/cli PR #1569 exact base ec83887a16f780f32fba4b7d710bad262dba3a22 head cea7b2afba91f11f1cdbb1785ea97ba61159520c. Immutable Git objects only; never worktree. Repo /home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/toolkit-bridge-upgrade. MUST return substantive non-empty report. Inspect all changed source/tests. Verify endpoint response shape/exact tag fail closed; arbitrary cancellation every stage; transport-only cache fallback; stable package SemVer; strict branded prefetch; target channels; semantic stable filters; exact metadata; 404/source affinity/trusted pagination/GHCR scope+redirect/SHA/delta/cache/offline/installer/CodeQL exact origin. 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; empty forbidden.
ec83887a16f780f32fba4b7d710bad262dba3a22 cea7b2afba91f11f1cdbb1785ea97ba61159520c ec83887a16f780f32fba4b7d710bad262dba3a22 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 | 170 +++++- packages/cli/src/lib/delta-upgrade.ts | ...
...output truncated...
Full output saved to: /home/byk/.local/share/opencode/tool-output/tool_0899b33c1001gdRL6q47xqfRac
<!-- 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..b3039d86b 100644 --- a/packages/cli/src/commands/cli/upgrade.ts +++ b/packages/cli/src/commands/cli/upgrade.ts @@ -1,414 +1,465 @@ /** * 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..8a96e9361 100644
--- a/packages/cli/src/lib/binary.ts
+++ b/packages/cli/src/lib/binary.ts
@@ -1,465 +1,615 @@
/**
* 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/upgrade.ts b/packages/cli/src/lib/upgrade.ts
index 945adc48d..b6235a6b6 100644
--- a/packages/cli/src/lib/upgrade.ts
+++ b/packages/cli/src/lib/upgrade.ts
@@ -1,189 +1,286 @@
/**
* 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/ghcr.ts b/packages/cli/src/lib/ghcr.ts index 43d6deb7e..fb441e057 100644 --- a/packages/cli/src/lib/ghcr.ts +++ b/packages/cli/src/lib/ghcr.ts @@ -1,520 +1,558 @@ /** * 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/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,627 +1,725 @@
/** 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/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
@@ -501,240 +501,248 @@ export class ResolutionError extends CliError {
override format(): string {
return this.message;
}
}
/**
* Build a {@link ValidationError} message with Try: examples and optional Note: section.
*
* Matches {@link buildResolutionMessage} / {@link buildContextMessage} formatting so
* agent consumers get actionable recovery commands plus diagnostic context in a
* predictable shape. Always pass `field` when constructing the error — unfielded
* validation errors collapse into one Sentry fingerprint. ...
...output truncated...
Full output saved to: /home/byk/.local/share/opencode/tool-output/tool_0899c87ab001kf6Ven4aRJnfzd
cea7b2afba91f11f1cdbb1785ea97ba61159520c:packages/cli/test/commands/cli/upgrade.test.ts:1008: expect(setupCall?.args).not.toContain("--ensure-auth-scopes");
cea7b2afba91f11f1cdbb1785ea97ba61159520c:packages/cli/test/commands/cli/upgrade.test.ts:1039: test("downloads nightly binary from GHCR for nightly channel", async () => {
cea7b2afba91f11f1cdbb1785ea97ba61159520c:packages/cli/test/commands/cli/upgrade.test.ts:1044: // GHCR flow: token exchange → manifest → blob redirect → blob download
cea7b2afba91f11f1cdbb1785ea97ba61159520c:packages/cli/test/commands/cli/upgrade.test.ts:1069: digest: "sha256:abc123",
cea7b2afba91f11f1cdbb1785ea97ba61159520c:packages/cli/test/commands/cli/upgrade.test.ts:1084: if (urlStr.includes("/blobs/sha256:abc123")) {
cea7b2afba91f11f1cdbb1785ea97ba61159520c:packages/cli/test/commands/cli/upgrade.test.ts:1086: ...
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,
...
cea7b2afba91f11f1cdbb1785ea97ba61159520c:packages/cli/src/commands/cli/import.ts:331: // The trust gate protects against redirecting a token to a malicious host, cea7b2afba91f11f1cdbb1785ea97ba61159520c:packages/cli/src/lib/db/schema.ts:703: // instead of being swallowed and replaced with the original error cea7b2afba91f11f1cdbb1785ea97ba61159520c:packages/cli/src/lib/proguard.ts:66: // codeql[js/weak-cryptographic-algorithm] cea7b2afba91f11f1cdbb1785ea97ba61159520c:packages/cli/src/lib/sourcemap/inject.ts:47: /** Discriminated location of the sourcemap (external file vs inline data URL). */ cea7b2afba91f11f1cdbb1785ea97ba61159520c:packages/cli/test/lib/init/formatters.test.ts:735: // tripped CodeQL's "incomplete URL substring sanitization" rule —
cea7b2afba91f11f1cdbb1785ea97ba61159520c:.github/workflows/ci.yml:470: registry: ghcr.io
cea7b2afba91f11f1cdbb1785ea97ba61159520c:.github/workflows/ci.yml:471: repo: getsentry/cli
cea7b2afba91f11f1cdbb1785ea97ba61159520c:.github/workflows/ci.yml:553: registry: ghcr.io
cea7b2afba91f11f1cdbb1785ea97ba61159520c:.github/workflows/ci.yml:554: repo: getsentry/cli
cea7b2afba91f11f1cdbb1785ea97ba61159520c:.github/workflows/cleanup-nightlies.yml:28: run: echo "${{ secrets.GITHUB_TOKEN }}" | oras login ghcr.io -u ${{ github.actor }} --password-stdin
cea7b2afba91f11f1cdbb1785ea97ba61159520c:.github/workflows/cleanup-nightlies.yml:34: REPO="ghcr.io/getsentry/cli"
apps/cli-docs/public/install apps/cli-docs/src/components/InstallSelector.astro packages/cli/install 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
1 #!/bin/bash
2 set -euo pipefail
3
4 RED='\033[0;31m'
5 MUTED='\033[0;2m'
6 NC='\033[0m'
7
8 # Sentry error telemetry — fire-and-forget error reporting via envelope API.
9 # Uses the CLI's public write-only DSN. No PII collected.
10 # Opt-out: SENTRY_CLI_NO_TELEMETRY=1
11 SENTRY_DSN_KEY="1188a86f3f8168f089450587b00bca66"
12 SENTRY_INGEST="https://o1.ingest.us.sentry.io"
13 SENTRY_PROJECT_ID="4510776311808000"
14
15 # Generate a UUID for the event. Tries /proc, uuidgen, then awk fallback.
16 gen_uuid() {
17 if [[ -r /proc/sys/kernel/random/uuid ]]; then
18 cat /proc/sys/kernel/random/uuid
19 elif command -v uuidgen >/dev/null 2>&1; then
20 uuidgen | tr '[:upper:]' '[:lower:]'
21 else
22 awk 'BEGIN{srand();for(i=1;i<=32;i++)printf "%c",substr("0123456789abcdef",int(rand()*16)+1,1);print ""}'
23 fi
24 }
25
26 # Send an error event to Sentry. ...
...output truncated...
Full output saved to: /home/byk/.local/share/opencode/tool-output/tool_0899d8a0a001W4e16GwIuuaYFL
+ new Response(JSON.stringify([{ tag_name: "cli@99.0.0" }]), {
status: 200,
headers: { "Content-Type": "application/json" },
})) as typeof fetch;
const func = await upgradeCommand.loader();
const { context, getStdout, restore } = createMockContext();
restoreStderr = restore;
await func.call(context, { check: true, method: "curl", json: true });
const data = JSON.parse(getStdout()) as UpgradeResult;
expect(data.action).toBe("checked");
expect(data.targetVersion).toBe("99.0.0");
expect(data.warnings).toContain("Run 'sentry cli upgrade' to update.");
});
test("check mode with version shows versioned command", async () => {
- globalThis.fetch = (async () =>
- new Response(JSON.stringify({ tag_name: "v99.0.0" }), {
+ globalThis.fetch = (async (url) => {
+ const response ...
170 test("parses valid methods", () => {
171 expect(parseInstallationMethod("curl")).toBe("curl");
172 expect(parseInstallationMethod("brew")).toBe("brew");
173 expect(parseInstallationMethod("npm")).toBe("npm");
174 expect(parseInstallationMethod("pnpm")).toBe("pnpm");
175 expect(parseInstallationMethod("bun")).toBe("bun");
176 expect(parseInstallationMethod("yarn")).toBe("yarn");
177 });
178
179 test("parses case-insensitively", () => {
180 expect(parseInstallationMethod("NPM")).toBe("npm");
181 expect(parseInstallationMethod("Curl")).toBe("curl");
182 expect(parseInstallationMethod("YARN")).toBe("yarn");
183 });
184
185 test("throws on invalid method", () => {
186 expect(() => parseInstallationMethod("pip")).toThrow("Invalid method: pip");
187 expect(() => parseInstallationMethod("apt")).toThrow("Invalid method: apt");
188 expect(() => ...
cea7b2afba91f11f1cdbb1785ea97ba61159520c:packages/cli/src/commands/cli/upgrade.ts:10: * - nightly: tracks the rolling nightly prerelease built from main
cea7b2afba91f11f1cdbb1785ea97ba61159520c:packages/cli/src/lib/upgrade.ts:24:import { prerelease as semverPrerelease, valid as semverValid } from "semver";
cea7b2afba91f11f1cdbb1785ea97ba61159520c:packages/cli/src/lib/upgrade.ts:134: .filter((release) => !(release.draft || release.prerelease))
cea7b2afba91f11f1cdbb1785ea97ba61159520c:packages/cli/src/lib/upgrade.ts:511: | Array<{ tag_name?: string; draft?: boolean; prerelease?: boolean }>;
cea7b2afba91f11f1cdbb1785ea97ba61159520c:packages/cli/src/lib/upgrade.ts:593: `${source} returned an invalid stable version`
cea7b2afba91f11f1cdbb1785ea97ba61159520c:packages/cli/src/lib/upgrade.ts:1200: * so the URL points to the prerelease assets regardless of the version string.
cea7b2afba91f11f1cdbb1785ea97ba61159520c:packages/cli/test/commands/cli/upgrade.test.ts:1303: ...
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
@@ -62,105 +62,105 @@ afterEach(() => {
function mockFetch(
fn: (url: string | URL | Request, init?: RequestInit) => Promise<Response>
): void {
globalThis.fetch = fn as typeof globalThis.fetch;
}
// ============================================================================
// Helpers
// ============================================================================
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(),
...
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,71 +1,77 @@
/**
* 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 ...
cea7b2afba91f11f1cdbb1785ea97ba61159520c fix(cli): validate release response shapes --- 7d62ffa8317f9b98be0dc4afec0a927c1cabb0b7 fix(cli): preserve paginated request cancellation --- 878459c490576dbb7abb76d3a3dc1b5d9fc62c1f fix(cli): fail closed on upgrade metadata --- e22f0eba66298a7228a988a4adcf1ec5be85227b fix(cli): preserve source and cancellation provenance --- dcf6719995800d28d6f88a8c709abe4aa3d0b6db test(cli): validate GitHub request origin --- f1c10a6cb1a753b8c4b8c613cd78735c3f12cba6 fix(cli): preserve pinned upgrade contracts --- e565b1afb39a341605adc883de8812851de7d581 fix(cli): validate stable upgrade metadata --- 4a1d5bad105c006193c68459175f5eb3c7f78e08 test(cli): update pinned version fixture --- 2f9b5d0d26124d445a916b8bfc584ad518720584 fix(cli): isolate pinned upgrade resolution --- c6030fbf4d0c190f037c86d832edb6680d3e54ab fix(cli): bind nightly manifests to versions --- b6484e0fb1eb0f8bce68272d538f5c3b26cd5bd5 fix(cli): validate resolved upgrade ...
dcf671999 test(cli): validate GitHub request origin
packages/cli/test/commands/cli/upgrade.test.ts | 8 +++++---
1 file changed, 5 insertions(+), 3 deletions(-)
commit dcf6719995800d28d6f88a8c709abe4aa3d0b6db
Author: Burak Yigit Kaya <byk@sentry.io>
AuthorDate: Thu Sep 10 03:52:45 2026 +0000
Commit: Burak Yigit Kaya <byk@sentry.io>
CommitDate: Thu Sep 10 03:52:45 2026 +0000
test(cli): validate GitHub request origin
diff --git a/packages/cli/test/commands/cli/upgrade.test.ts b/packages/cli/test/commands/cli/upgrade.test.ts
index b67847fb7..bcde490a2 100644
--- a/packages/cli/test/commands/cli/upgrade.test.ts
+++ b/packages/cli/test/commands/cli/upgrade.test.ts
@@ -1213,163 +1213,165 @@ describe("sentry cli upgrade — migrateToStandaloneForNightly (child_process.sp
return new Response(null, { status: 200 });
}
if (request.includes("ghcr.io/token")) {
return new Response(JSON.stringify({ token: "test-token" }), {
status: 200,
...
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
@@ -47,190 +47,184 @@ function createMockContext(overrides: Partial<{ execPath: string }> = {}): {
write: (s: string) => {
errors.push(s);
return true;
},
},
};
return {
context,
getStderr: () => stderrChunks.join(""),
getStdout: () => stdoutChunks.join(""),
errors,
restore: () => {
process.stderr.write = origWrite;
},
};
}
describe("feedbackCommand.func", () => {
test("throws ValidationError for empty message", async () => {
// Access func through loader
const func = await feedbackCommand.loader();
const mockContext = {
stdout: { write: vi.fn(() => true) },
stderr: { write: vi.fn(() => true) },
};
await expect(func.call(mockContext, {}, ...
...output truncated...
Full output saved to: /home/byk/.local/share/opencode/tool-output/tool_0899f7b40001lose8JH0Iwtw2t
+ value: upgradeSourceKey(source),
+ });
+ return normalized;
+}
+
function getPrimaryUpgradeSource(): UpgradeSource {
if (!PRIMARY_UPGRADE_SOURCE) {
throw new Error("No primary upgrade source is configured");
}
return PRIMARY_UPGRADE_SOURCE;
}
const instrument: InstrumentHook = (name, fn) =>
withTracing(name, "http.client", fn);
function patchCacheKey(fromVersion: string, toVersion: string): string {
return `patch-chain:${fromVersion}-${toVersion}`;
}
function instrumentCache(base: PatchCache): PatchCache {
return {
load(currentVersion, targetVersion) {
const key = patchCacheKey(currentVersion, targetVersion);
return withTracingSpan(key, "cache.get", async (span) => {
span.setAttribute("cache.key", [key]);
const result = await base.load(currentVersion, targetVersion);
...
cea7b2afba91f11f1cdbb1785ea97ba61159520c:pnpm-lock.yaml:221: binpatch:
cea7b2afba91f11f1cdbb1785ea97ba61159520c:pnpm-lock.yaml:2696: binpatch@0.4.2:
cea7b2afba91f11f1cdbb1785ea97ba61159520c:pnpm-lock.yaml:7801: binpatch@0.4.2: {}
1 import { join } from "node:path";
2 import { makeCache, type PatchCache, type PatchChain } from "binpatch";
3 import { getConfigDir } from "./db/index.js";
4
5 export type { ChainMeta, PatchStepMeta } from "binpatch";
6 // biome-ignore lint/performance/noBarrelFile: preserve the existing cache API
7 export { chainFileName, patchFileName } from "binpatch";
8
9 function cache(): PatchCache {
10 return makeCache(join(getConfigDir(), "patch-cache"));
11 }
12
13 export function savePatchesToCache(
14 chain: Pick<PatchChain, "patches" | "expectedSha256">,
15 steps: { fromVersion: string; toVersion: string }[]
16 ): Promise<void> {
17 return cache().save(chain, steps);
...
490 *
491 * @param signal - Optional AbortSignal to cancel the request
492 * @returns Latest version string (without 'v' prefix)
493 * @throws {UpgradeError} When fetch fails or response is invalid
494 * @throws {Error} AbortError if signal is aborted
495 */
496 export async function fetchLatestFromGitHubWithSource(
497 signal?: AbortSignal,
498 sources: readonly UpgradeSource[] = UPGRADE_SOURCES
499 ): Promise<ResolvedUpgradeVersion> {
500 const resolved = await resolveUpgradeSource({
501 getProbeUrl: getGitHubLatestReleaseUrl,
502 signal,
503 sources,
504 });
505 let response = resolved.response;
506 const visitedPages = new Set([getGitHubLatestReleaseUrl(resolved.source)]);
507 const versions: string[] = [];
508 while (true) {
509 const data = (await response.json()) as
510 | { tag_name?: string }
511 | Array<{ tag_name?: string; draft?: boolean; prerelease?: boolean ...
commit cea7b2afba91f11f1cdbb1785ea97ba61159520c
Author: Burak Yigit Kaya <byk@sentry.io>
AuthorDate: Thu Sep 10 04:35:48 2026 +0000
Commit: Burak Yigit Kaya <byk@sentry.io>
CommitDate: Thu Sep 10 04:35:48 2026 +0000
fix(cli): validate release response shapes
diff --git a/packages/cli/src/lib/ghcr.ts b/packages/cli/src/lib/ghcr.ts
index 3cba357f8..fb441e057 100644
--- a/packages/cli/src/lib/ghcr.ts
+++ b/packages/cli/src/lib/ghcr.ts
@@ -1,194 +1,179 @@
/**
* 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 ...
ec83887a16f780f32fba4b7d710bad262dba3a22 cea7b2afba91f11f1cdbb1785ea97ba61159520c ec83887a16f780f32fba4b7d710bad262dba3a22 b8d084021f9abb67a916c80ae2d18e3ae7c4552cad4c5a7a7e1d11385b474ff4 - 17
/home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/toolkit-bridge-upgrade/packages/cli/src/lib/ghcr.ts:234-242 accepts any truthy token and returns it as a string. { "token": {} } crosses the typed boundary./home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/toolkit-bridge-upgrade/packages/cli/src/lib/ghcr.ts:253-277 casts arbitrary JSON to OciManifest without validating schemaVersion, layers, annotations, digests, sizes, or media types./home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/toolkit-bridge-upgrade/packages/cli/src/lib/upgrade.ts:617-645 therefore selects a nightly source from a partial payload such as {annotations:{version:"1.2.3-dev.1"}}. Check mode can report and cache that version even though the response is not an OCI manifest.This violates the requested endpoint-shape and fail-closed contracts. The final commit hardens GitHub release responses but leaves the equivalent GHCR trust boundary unchecked.
Fix: validate token responses as objects containing a non-empty string token. Parse OCI manifests through one runtime validator before returning them. Require schema version 2, an array of structurally valid layers, string annotations, non-negative finite sizes, and valid sha256:<hex> digests where consumed.
Deterministic regressions:
{token:{}} with HTTP 200 and assert getAnonymousToken() rejects with UpgradeError before any manifest request.fetchLatestNightlyVersionWithSource() HTTP-200 payloads that are arrays, null, omit layers, use non-array layers, or contain malformed layer fields. Assert terminal metadata failure and zero legacy-source requests.* 🟡 (04:44) Commit `cea7b2afba91f11f1cdbb1785ea97ba61159520c` by Burak Yigit Kaya (`byk@sentry.io`), titled `fix(cli): validate release response shapes`, changed `packages/cli/src/lib/ghcr.ts`; authored and committed at Thu Sep 10 04:35:48 2026 +0000. * 🟡 (04:44) `extractReleaseVersions(data, source)` now accepts `data: unknown` and validates the GitHub release-response shape according to `sour…
* 🟡 (04:43) `pnpm-lock.yaml` pins `binpatch@0.4.2`; the package is referenced under the project dependencies and resolved in the lockfile. * 🟡 (04:43) The patch-cache module imports `makeCache`, `PatchCache`, and `PatchChain` from `binpatch`; re-exports `ChainMeta`, `PatchStepMeta`, `chainFileName`, and `patchFileName`; and preserves the existing cache API using `// biome-ignore lint/performanc…
* 🟡 (04:41) Commit `dcf6719995800d28d6f88a8c709abe4aa3d0b6db` (`test(cli): validate GitHub request origin`, authored and committed by Burak Yigit Kaya `<byk@sentry.io>` at Thu Sep 10 03:52:45 2026 +0000) changed `packages/cli/test/commands/cli/upgrade.test.ts` with 5 insertions and 3 deletions. * 🟡 (04:41) In `packages/cli/test/commands/cli/upgrade.test.ts`, the test `"validates an npm stable p…
* 🟡 (04:40) `packages/cli/test/lib/delta-upgrade.mocked.test.ts` adds parameterized coverage that stable release discovery excludes semantic prereleases even when GitHub reports `prerelease: false`: Toolkit/default source fixture `cli@0.14.0-dev.1` is excluded in favor of `cli@0.14.0`, and `LEGACY_UPGRADE_SOURCE` fixture `0.14.0-dev.1` is excluded in favor of `0.14.0`. * 🟡 (04:40) `packages/cli…
Date: Sep 10, 2026 * 🟡 (04:40) At commit `cea7b2afba91f11f1cdbb1785ea97ba61159520c`, `packages/cli/src/commands/cli/upgrade.ts:10` documents the `nightly` channel as tracking the rolling nightly prerelease built from `main`. * 🟡 (04:40) `packages/cli/src/lib/upgrade.ts:24` imports `prerelease` as `semverPrerelease` and `valid` as `semverValid` from `semver`. * 🟡 (04:40) Stable GitHub release s…
Date: Sep 10, 2026 * 🔴 (04:40) User specified upgrade checks “never uses the cached target after %s”; after the relevant responses, the upgrade flow must report errors rather than reuse a cached target. * 🔴 (04:40) User stated: “Switch to nightly and use npm method → triggers migration.” * 🟡 (04:40) Upgrade tests added `resolves a pinned check target from its exact source`, ensuring a pinned v…
Date: Sep 10, 2026 * 🔴 (04:39) User specified that Sentry error telemetry in `packages/cli/install` is fire-and-forget and must never block installation or propagate failures. * 🔴 (04:39) User specified that telemetry must never fail the installation script; `packages/cli/install:34` uses `set +e` inside telemetry handling to enforce this. * 🟡 (04:39) `packages/cli/install` documents Sentry er…
Date: Sep 10, 2026 * 🟡 (04:39) Tool output for commit `cea7b2afba91f11f1cdbb1785ea97ba61159520c` was truncated and saved in full to `/home/byk/.local/share/opencode/tool-output/tool_0899c87ab001kf6Ven4aRJnfzd`. * 🟡 (04:39) `packages/cli/test/commands/cli/upgrade.test.ts:1039` includes `test("downloads nightly binary from GHCR for nightly channel", async () => {`. * 🟡 (04:39) `packages/cli/test…
Date: Sep 10, 2026 * 🔴 (04:38) User specified that callers must always pass `field` when constructing a `ValidationError`; unfielded validation errors collapse into one Sentry fingerprint. * 🟡 (04:38) `packages/cli/src/lib/errors.ts` adds `buildValidationMessage(headline: string, examples: string[], note?: string): string`, formatting a validation headline followed by an optional blank line and…
Date: Sep 10, 2026 * 🟡 (04:38) `packages/cli/src/lib/ghcr.ts` now imports `valid as semverValid` from `semver`, `PRIMARY_UPGRADE_SOURCE` and `type UpgradeSource` from `./binary.js`, and `UpgradeTransportError` alongside `UpgradeError` from `./errors.js`. * 🟡 (04:38) `packages/cli/src/lib/ghcr.ts` defines `NIGHTLY_VERSION_REGEX = /^\d+\.\d+\.\d+-dev\.\d+$/`; `getNightlyVersion()` now rejects ann…
Date: Sep 10, 2026 * 🔴 (04:38) User requires `detectInstallationMethod()` to always check Homebrew first because stored installation information may be stale; the Homebrew realpath check is cheap and authoritative. * 🔴 (04:38) User requires upgrade file-readiness handling to throw `UpgradeError` when the file never becomes visible or stays empty. * 🟡 (04:38) `packages/cli/src/lib/upgrade.ts` i…
Date: Sep 10, 2026 * 🔴 (04:37) User stated package managers always need network access to fetch and install packages; `--offline` upgrades are supported only for curl-installed binaries. * 🔴 (04:37) User requires nightly builds to always use curl with a GitHub lookup, regardless of the current installation method. * 🔴 (04:37) User stated `sentry cli upgrade nightly` means “Switch to nightly ch…
Date: Sep 10, 2026 * 🔴 (04:36) User requires security/supply-chain reviews to be strictly read-only: never modify anything, use immutable Git objects only, and never use the worktree. * 🔴 (04:36) User requires every final security/supply-chain review to return a substantive, non-empty report; empty output is forbidden. * 🟡 [requested-review] (04:36) User requested a final security/supply-chain…