Dashboard › cli › Session 1AOpuRTbd9rM
1AOpuRTbd9rM0NDmTREAD-ONLY correctness merge-gate review of getsentry/cli PR #1569. Worktree /home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/toolkit-bridge-upgrade. Exact base ec83887a16f780f32fba4b7d710bad262dba3a22, exact head 5ffa5cd0fe02c0a5dc5bec9a8a099f4135c0b447. Verify revisions, inspect every changed file and relevant tests. Focus on bridge source selection, genuine-404-only fallback, Toolkit release pagination including hostile/cyclic Link values, SemVer selection, explicit-source status handling, GHCR in-flight cancellation, source affinity, stable/nightly delta/full download, cache/offline behavior, prefetch/changelog, and API compatibility. The GitHub repository probe before GHCR nightly access is intentional because GHCR missing-package token responses are ambiguous 401/403; do not propose removing it. The verified delta cache is intentionally keyed by version pair because dual-published artifacts must be byte-identical; do not require source namespacing absent a concrete integrity bypass. Return a non-empty findings-first report labeled PASS/CONCERN/MUST-FIX/BLOCKED with exact current file:line evidence and deterministic regressions for defects. Include residual gaps. Final line exactly MERGE or DO-NOT-MERGE. Do not modify anything.
<path>/home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/toolkit-bridge-upgrade/AGENTS.md</path> <type>file</type> <content> 1: # Jared (Outpost agent) 2: 3: Autonomous GitHub coding agent. Work in `/workspace/repo`. 4: 5: ## Model tiers 6: 7: The primary model is chosen per event (see `src/agents/models.ts`): heavy for 8: code-producing situations, cheaper for lightweight ones. 9: 10: | Role | Subagent | Model | 11: | --- | --- | --- | 12: | Triage / plan / review (heavy) | (primary Jared) | Claude Opus 4.8 | 13: | Triage / plan / review (light) | (primary Jared) | xAI Grok 4.3 | 14: | Explore | `explore` | OpenAI gpt-5-mini | 15: | Implement | `implement` | Moonshot kimi-k2.7-code | 16: | Ship (commit/push/PR) | `ship` | xAI Grok (`grok-build-0.1`) | 17: 18: Pipeline: triage → explore → plan → implement → review → ship. 19: (`worker` is a deprecated alias of `implement`.) 20: 21: Operators also talk to Jared directly from the Outpost dashboard. ...
No files found
<path>/home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/toolkit-bridge-upgrade</path> <type>directory</type> <entries> .craft.yml .git .github/ .gitignore .lore.md .nojekyll .npmrc AGENTS.md apps/ CHANGELOG.md codemods/ docs/ LICENSE.md node_modules/ package.json packages/ pnpm-lock.yaml pnpm-workspace.yaml README.md (19 entries) </entries>
No files found
<path>/home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/toolkit-bridge-upgrade/.lore.md</path>
<type>file</type>
<content>
1: <!-- Managed by lore (https://github.com/BYK/loreai) — manual edits are imported on next session. -->
2:
3: ## Long-term Knowledge
4:
5: ### Architecture
6:
7: <!-- lore:01a02357-f0a8-7fdb-9e9d-b0710cfe65c9 -->
8: * **API output binary and HTTP contracts**: Always preserve strict output-mode and binary-safety contracts. `sentry api` must preserve `rawApiRequest()` status text; empty textual non-2xx bodies must produce an HTTP status/request fallback, and JSON mode must expose `{status, statusText, body}` so empty success and error responses are distinguishable. Binary `Uint8Array` successes bypass formatters and remain raw; binary errors emit only status/content-type/byte-count summaries, never bytes or JSON coercion. Prompts must never block scripted runs or interleave with stdout JSON.
9:
10: <!-- ...
<path>/home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/toolkit-bridge-upgrade/package.json</path>
<type>file</type>
<content>
1: {
2: "name": "toolkit-cli-workspace",
3: "version": "0.0.0",
4: "private": true,
5: "type": "module",
6: "packageManager": "pnpm@10.11.0",
7: "engines": {
8: "node": ">=20.0"
9: },
10: "scripts": {
11: "tsx": "pnpm --filter sentry run tsx",
12: "cli": "pnpm --filter sentry run cli",
13: "dev": "pnpm --filter sentry run dev",
14: "build": "pnpm --filter sentry run build",
15: "build:all": "pnpm --filter sentry run build:all",
16: "bundle": "pnpm --filter sentry run bundle",
17: "typecheck": "pnpm --filter sentry run typecheck",
18: "lint": "pnpm --filter sentry run lint",
19: "lint:fix": "pnpm --filter sentry run lint:fix",
20: "test": "pnpm --filter sentry run test",
21: "test:unit": "pnpm --filter sentry run test:unit",
22: "test:changed": "pnpm --filter ...
5ffa5cd0fe02c0a5dc5bec9a8a099f4135c0b447 ec83887a16f780f32fba4b7d710bad262dba3a22 5ffa5cd0fe02c0a5dc5bec9a8a099f4135c0b447 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 | 114 ++++-- packages/cli/src/lib/binary.ts | 154 +++++++- packages/cli/src/lib/delta-upgrade.ts | 139 +++++-- ...
...output truncated...
Full output saved to: /home/byk/.local/share/opencode/tool-output/tool_088f9ac4c001cJZMDKW9jlhBl6
* each with a trailing separator so `startsWith` matches a directory boundary
* (e.g. `~/.local/bin/` won't match `~/.local/binaries/`). Pure — takes home
* and env — so it can be unit-tested; `getKnownCurlPaths` memoizes the result.
*/
export function buildKnownCurlPaths(
homeDir: string,
env: NodeJS.ProcessEnv
): string[] {
const paths = KNOWN_CURL_DIRS.map((dir) => join(homeDir, dir) + sep);
// Honor an absolute XDG_BIN_HOME, matching determineInstallDir's precedence.
const xdgBinHome = env.XDG_BIN_HOME;
if (xdgBinHome && isAbsolute(xdgBinHome)) {
// join(dir, ".") strips any trailing separator so we don't emit a double
// separator (e.g. `/custom/bin//`) that would break the startsWith checks.
paths.push(join(xdgBinHome, ".") + sep);
}
return paths;
}
/**
* Memoized known curl paths. ...
1096 packages/cli/src/commands/cli/upgrade.ts 739 packages/cli/src/lib/binary.ts 702 packages/cli/src/lib/delta-upgrade.ts 546 packages/cli/src/lib/ghcr.ts 784 packages/cli/src/lib/release-notes.ts 1367 packages/cli/src/lib/upgrade.ts 501 packages/cli/src/lib/version-check.ts 239 packages/cli/test/commands/cli.test.ts 1249 packages/cli/test/commands/cli/upgrade.test.ts 916 packages/cli/test/lib/binary.test.ts 500 packages/cli/test/lib/delta-upgrade.mocked.test.ts 2015 packages/cli/test/lib/delta-upgrade.test.ts 609 packages/cli/test/lib/ghcr.test.ts 406 packages/cli/test/lib/release-notes.test.ts 2479 packages/cli/test/lib/upgrade.test.ts 670 packages/cli/test/lib/version-check.test.ts 14818 total
diff --git a/packages/cli/src/commands/cli/upgrade.ts b/packages/cli/src/commands/cli/upgrade.ts
index 8d8282368..610a12025 100644
--- a/packages/cli/src/commands/cli/upgrade.ts
+++ b/packages/cli/src/commands/cli/upgrade.ts
@@ -25,6 +25,7 @@ import {
LEGACY_INSTALL_SUBDIR,
releaseLock,
samePath,
+ type UpgradeSource,
} from "../../lib/binary.js";
import { buildCommand } from "../../lib/command.js";
import { CLI_VERSION } from "../../lib/constants.js";
@@ -54,6 +55,8 @@ import {
NIGHTLY_TAG,
type OfflineMode,
parseInstallationMethod,
+ resolveExistingUpgradeVersion,
+ resolveLatestUpgradeVersion,
VERSION_PREFIX_REGEX,
versionExists,
} from "../../lib/upgrade.js";
@@ -172,8 +175,13 @@ async function resolveTargetWithFallback(opts: {
* clearing the version cache before the offline path can read it). ...
diff --git a/packages/cli/src/lib/upgrade.ts b/packages/cli/src/lib/upgrade.ts
index 945adc48d..376eb6f76 100644
--- a/packages/cli/src/lib/upgrade.ts
+++ b/packages/cli/src/lib/upgrade.ts
@@ -19,25 +19,34 @@ import {
} 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 { valid as semverValid } from "semver";
import {
acquireLock,
cleanupOldBinary,
+ compareVersions,
determineInstallDir,
fetchWithUpgradeError,
- GITHUB_RELEASES_URL,
getBinaryDownloadUrl,
getBinaryFilename,
getBinaryPaths,
getGitHubHeaders,
+ getGitHubLatestReleaseUrl,
+ getGitHubReleaseByTagUrl,
+ getGitHubRepositoryUrl,
getPlatformBinaryName,
type InstallationMethod,
isNightlyVersion,
KNOWN_CURL_DIRS,
+ PRIMARY_UPGRADE_SOURCE,
releaseLock,
+ resolveUpgradeSource,
+ UPGRADE_SOURCES,
+ type ...
Found 100 matches (more matches available)
/home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/toolkit-bridge-upgrade/packages/cli/src/lib/upgrade.ts:
Line 31: getBinaryDownloadUrl,
Line 52: import { attemptDeltaUpgrade, type DeltaResult } from "./delta-upgrade.js";
Line 56: downloadNightlyBlob,
Line 57: fetchManifest,
Line 58: fetchNightlyManifest,
Line 61: getAnonymousToken,
Line 512: export async function fetchLatestFromGitHub(
Line 597: const token = await getAnonymousToken(source, signal);
Line 598: const manifest = await fetchManifest(token, tag, signal, source);
Line 611: export async function fetchLatestNightlyVersion(
Line 635: export function fetchLatestVersion(
Line 640: return fetchLatestNightlyVersion();
Line 643: ? ...
<path>/home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/toolkit-bridge-upgrade/packages/cli/src/lib/binary.ts</path>
<type>file</type>
<content>
1: /**
2: * Binary Management
3: *
4: * Shared utilities for installing, replacing, and managing the CLI binary.
5: * Used by both `setup --install` (fresh installs) and `upgrade` (self-updates).
6: */
7:
8: import { spawnSync } from "node:child_process";
9: import {
10: existsSync,
11: mkdirSync,
12: readFileSync,
13: renameSync,
14: unlinkSync,
15: writeFileSync,
16: } from "node:fs";
17: import { chmod, copyFile, mkdir, realpath, unlink } from "node:fs/promises";
18: import { delimiter, dirname, isAbsolute, join, resolve, sep } from "node:path";
19: import { compare as semverCompare } from "semver";
20: import { getUserAgent } from "./constants.js";
21: import {
22: buildTlsErrorDetail,
23: customFetch,
24: isTlsCertError,
25: } from "./custom-ca.js";
26: import { stringifyUnknown, ...
<path>/home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/toolkit-bridge-upgrade/packages/cli/src/lib/ghcr.ts</path> <type>file</type> <content> 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. ...
<path>/home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/toolkit-bridge-upgrade/packages/cli/test/lib/upgrade.test.ts</path>
<type>file</type>
<content>
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(() => parseInstallationMethod("")).toThrow("Invalid method: ");
189: });
190: });
191:
192: describe("fetchLatestFromGitHub", () => {
193: test("selects the latest CLI-prefixed Toolkit release", async () => {
194: const requests: string[] = [];
195: mockFetch(async (url) => {
196: requests.push(String(url));
197: return new Response(
198: ...
<path>/home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/toolkit-bridge-upgrade/packages/cli/test/lib/upgrade.test.ts</path>
<type>file</type>
<content>
818: test("checks GHCR for nightly version - version exists", async () => {
819: const manifest = { schemaVersion: 2, layers: [], annotations: {} };
820: mockFetch(async (url) => {
821: const u = String(url);
822: if (u === "https://api.github.com/repos/getsentry/toolkit") {
823: return new Response(null, { status: 200 });
824: }
825: if (u.includes("ghcr.io/token")) {
826: return new Response(JSON.stringify({ token: "tok" }), { status: 200 });
827: }
828: if (u.includes("/manifests/nightly-")) {
829: return new Response(JSON.stringify(manifest), { status: 200 });
830: }
831: return new Response(null, { status: 404 });
832: });
833:
834: const exists = await versionExists("curl", "0.14.0-dev.1772661724");
835: ...
diff --git a/packages/cli/test/lib/ghcr.test.ts b/packages/cli/test/lib/ghcr.test.ts
index e5c81d532..f185c5234 100644
--- a/packages/cli/test/lib/ghcr.test.ts
+++ b/packages/cli/test/lib/ghcr.test.ts
@@ -7,4 +7,5 @@
import { afterEach, beforeEach, describe, expect, test } from "vitest";
+import { UPGRADE_SOURCES } from "../../src/lib/binary.js";
import { UpgradeError } from "../../src/lib/errors.js";
import {
@@ -16,4 +17,5 @@ import {
GHCR_REPO,
GHCR_TAG,
+ GhcrManifestHttpError,
getAnonymousToken,
getNightlyVersion,
@@ -92,4 +94,17 @@ describe("getAnonymousToken", () => {
});
+ test("uses the selected source's GHCR repository", async () => {
+ mockFetch(async (url) => {
+ expect(String(url)).toContain("scope=repository:getsentry/toolkit:pull");
+ return new Response(JSON.stringify({ token: "toolkit-token" }), {
+ status: 200,
+ });
+ });
+
+ await expect(getAnonymousToken(UPGRADE_SOURCES[0])).resolves.toBe(
+ ...
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
@@ -25,6 +25,7 @@ import {
getBinaryDownloadUrl,
getBinaryFilename,
getBinaryPaths,
+ getGitHubReleaseByTagUrl,
getLegacyInstallDirs,
getPlatformBinaryName,
installBinary,
@@ -32,7 +33,10 @@ import {
isMusl,
releaseLock,
replaceBinarySync,
+ resolveUpgradeSource,
samePath,
+ UPGRADE_SOURCES,
+ UpgradeSourceNotFoundError,
} from "../../src/lib/binary.js";
import { UpgradeError } from "../../src/lib/errors.js";
@@ -40,9 +44,9 @@ describe("getBinaryDownloadUrl", () => {
test("builds correct URL for current platform", () => {
const url = getBinaryDownloadUrl("1.0.0");
- expect(url).toContain("/1.0.0/");
+ expect(url).toContain("/cli@1.0.0/");
expect(url).toStartWith(
- ...
<path>/home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/toolkit-bridge-upgrade/packages/cli/src/lib/delta-upgrade.ts</path>
<type>file</type>
<content>
1: /** Delta upgrade discovery and application backed by binpatch. */
2:
3: import { join } from "node:path";
4: // biome-ignore lint/performance/noNamespaceImport: Sentry SDK recommends namespace import
5: import * as Sentry from "@sentry/node-core/light";
6: import {
7: applyPatchChainInMemory,
8: extractStableChain as binpatchExtractStableChain,
9: filterAndSortChainTags as binpatchFilterAndSortChainTags,
10: validateChainStep as binpatchValidateChainStep,
11: type DeltaTelemetry,
12: type DeltaUnavailableReason,
13: type ExtractStableChainOpts,
14: type GitHubRelease,
15: getPatchFromVersion,
16: getPatchTargetSha256,
17: ghcrSource,
18: githubReleaseSource,
19: type InstrumentHook,
20: MAX_NIGHTLY_CHAIN_DEPTH,
21: makeCache,
22: OciClient,
23: type OciManifest,
24: ...
<path>/home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/toolkit-bridge-upgrade/packages/cli/src/commands/cli/upgrade.ts</path>
<type>file</type>
<content>
120: // "nightly" and "stable" as positional args select the channel rather than
121: // installing a specific version. Match case-insensitively for convenience.
122: const lower = positional?.toLowerCase();
123: if (lower === "nightly" || lower === "stable") {
124: return {
125: channel: lower,
126: versionArg: undefined,
127: };
128: }
129:
130: return {
131: channel: getReleaseChannel(),
132: versionArg: positional,
133: };
134: }
135:
136: /**
137: * Resolve the target version from the local cache (SQLite) instead of
138: * fetching from the network. Used by `--offline` and as automatic
139: * fallback when `fetchLatestVersion()` hits a network error. ...
<path>/home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/toolkit-bridge-upgrade/packages/cli/src/lib/release-notes.ts</path>
<type>file</type>
<content>
380: truncated,
381: originalCount,
382: };
383: }
384:
385: /**
386: * Merge extracted sections by category across multiple releases.
387: *
388: * Concatenates markdown from the same category and strips author attributions.
389: */
390: function mergeSectionsByCategory(releases: GitHubRelease[]): ChangeSection[] {
391: const sectionsByCategory = new Map<ChangeCategory, string[]>();
392:
393: for (const release of releases) {
394: if (!release.body) {
395: continue;
396: }
397: const sections = extractSections(release.body);
398: for (const section of sections) {
399: const stripped = stripAttributions(section.markdown);
400: const existing = sectionsByCategory.get(section.category) ?? [];
401: existing.push(stripped);
402: ...
Found 5 matches
/home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/toolkit-bridge-upgrade/packages/cli/src/lib/release-notes.ts:
Line 630: const { fromVersion, toVersion, maxItems, prefetchedReleases, source } =
Line 633: prefetchedReleases ?? (await fetchReleasesForChangelog(source));
Line 737: prefetchedReleases?: GitHubRelease[];
Line 761: prefetchedReleases,
Line 777: prefetchedReleases,
diff --git a/packages/cli/test/lib/release-notes.test.ts b/packages/cli/test/lib/release-notes.test.ts
index 78963b471..d60066802 100644
--- a/packages/cli/test/lib/release-notes.test.ts
+++ b/packages/cli/test/lib/release-notes.test.ts
@@ -8,18 +8,21 @@
* are tested via property-based tests in release-notes.property.test.ts.
*/
import { marked } from "marked";
-import { describe, expect, test } from "vitest";
+import { afterEach, beforeEach, describe, expect, test } from "vitest";
+import { UPGRADE_SOURCES } from "../../src/lib/binary.js";
import type { GitHubRelease } from "../../src/lib/delta-upgrade.js";
import {
buildChangelogSummary,
type ChangeCategory,
countListItems,
extractNightlyTimestamp,
extractSections,
+ fetchChangelog,
parseCommitMessages,
} from "../../src/lib/release-notes.js";
+import { mockFetch } from "../helpers.js";
// ─────────────────────────── Fixtures ──────────────────────────────────────
/** Realistic release body from ...
diff --git a/packages/cli/test/commands/cli.test.ts b/packages/cli/test/commands/cli.test.ts
index cd1514eb0..187ae70f4 100644
--- a/packages/cli/test/commands/cli.test.ts
+++ b/packages/cli/test/commands/cli.test.ts
@@ -124,7 +124,7 @@ describe("upgradeCommand.func", () => {
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@0.0.0-dev" }]), {
status: 200,
headers: { "Content-Type": "application/json" },
})) as typeof fetch;
@@ -144,9 +144,9 @@ describe("upgradeCommand.func", () => {
});
test("check mode shows update available", async () => {
- // curl uses GitHub API which returns { tag_name: "vX.X.X" }
+ // Curl uses the Toolkit GitHub release list with product-prefixed tags.
globalThis.fetch = (async () =>
- new Response(JSON.stringify({ tag_name: "v99.0.0" ...
Found 21 matches
/home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/toolkit-bridge-upgrade/packages/cli/test/lib/upgrade.test.ts:
Line 245: test("rejects GitHub release pagination outside the selected source", async () => {
Line 665: test("probes prefixed Toolkit tags and retains the selected source", async () => {
Line 708: test("does not fall back from an explicit selected source", async () => {
Line 1764: if (request.includes("/v2/getsentry/cli/manifests/nightly")) {
Line 1781: "https://ghcr.io/v2/getsentry/cli/manifests/nightly"
/home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/toolkit-bridge-upgrade/packages/cli/test/lib/ghcr.test.ts:
Line 96: test("uses the selected source's GHCR repository", async () => {
Line 192: test("uses the selected source's GHCR repository", async () => ...
<path>/home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/toolkit-bridge-upgrade/packages/cli/src/commands/cli/upgrade.ts</path>
<type>file</type>
<content>
620: export function resolveUpgradeInstallDir(
621: currentInstallDir: string,
622: pathEnv: string | undefined
623: ): string {
624: const legacyBinDir = join(homedir(), LEGACY_INSTALL_SUBDIR);
625: if (!samePath(currentInstallDir, legacyBinDir)) {
626: return currentInstallDir;
627: }
628:
629: // determineInstallDir with the legacy pin removed yields the XDG target.
630: const { SENTRY_INSTALL_DIR: _pinned, ...envWithoutPin } = process.env;
631: const xdgInstallDir = determineInstallDir(homedir(), envWithoutPin);
632: if (
633: !samePath(xdgInstallDir, legacyBinDir) &&
634: isInPath(xdgInstallDir, pathEnv)
635: ) {
636: return xdgInstallDir;
637: }
638: return currentInstallDir;
639: }
640:
641: /**
642: * Execute the standard upgrade path: download via ...
Found 5 matches
/home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/toolkit-bridge-upgrade/packages/cli/test/lib/binary.test.ts:
Line 101: response: expect.any(Response),
Line 132: 401, 403, 429, 500,
Line 428: test("returns response on success", async () => {
Line 432: const response = await fetchWithUpgradeError(
Line 437: expect(response.status).toBe(200);
No files found
Found 1 matches /home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/toolkit-bridge-upgrade/packages/cli/package.json: Line 111: "binpatch": "^0.4.2",
<path>/home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/toolkit-bridge-upgrade/packages/cli/node_modules/binpatch</path> <type>directory</type> <entries> dist/ LICENSE package.json README.md (4 entries) </entries>
<path>/home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/toolkit-bridge-upgrade/packages/cli/node_modules/binpatch/dist</path> <type>directory</type> <entries> index.d.ts index.js index.js.map (3 entries) </entries>
Found 5 matches
/home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/toolkit-bridge-upgrade/packages/cli/node_modules/binpatch/dist/index.js:
Line 610: function makeCache(cacheDir) {
Line 654: async function resolveAndApply(opts) {
Line 1038: function ghcrSource(config) {
Line 1225: function githubReleaseSource(config) {
Line 1318: export { BinpatchError, MAX_NIGHTLY_CHAIN_DEPTH, MAX_OUTPUT_SIZE, MAX_STABLE_CHAIN_DEPTH, OciClient, PATCH_TAG_PREFIX, SIZE_THRESHOLD_RATIO, addDiffChunk, applyPatch, applyPatchChainInMemory, applyPatchToMemory, chainFileName, extractSha256, extractStableChain, filterAndSortChainTags, getPatchFromVersion, getPatchTargetSha256, getStableTargetSha256, ghcrSource, githubReleaseSource, makeCache, offtin, parsePatchHeader, patchFileName, resolveAndApply, safeProgress, validateChainStep };
<path>/home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/toolkit-bridge-upgrade/packages/cli/node_modules/binpatch/dist/index.js</path>
<type>file</type>
<content>
600: files = await readdir(cacheDir);
601: } catch (err) {
602: if (isNotFound(err)) return;
603: throw err;
604: }
605: await Promise.all(
606: files.map((file) => unlink(join(cacheDir, file)).catch(() => {
607: }))
608: );
609: }
610: function makeCache(cacheDir) {
611: return {
612: save: (chain, steps) => savePatchesToCache(cacheDir, chain, steps),
613: load: (currentVersion, targetVersion) => loadCachedChain(cacheDir, currentVersion, targetVersion),
614: cleanup: () => cleanupPatchCache(cacheDir),
615: clear: () => clearPatchCache(cacheDir)
616: };
617: }
618:
619: // src/contract.ts
620: var MAX_STABLE_CHAIN_DEPTH = 10;
621: var MAX_NIGHTLY_CHAIN_DEPTH = 30;
622: var SIZE_THRESHOLD_RATIO = 0.6;
623: var PATCH_TAG_PREFIX = "patch-";
624: ...
<path>/home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/toolkit-bridge-upgrade/packages/cli/node_modules/binpatch/dist/index.js</path>
<type>file</type>
<content>
747: // src/sources/oci.ts
748: var REQUEST_TIMEOUT = 1e4;
749: var MAX_RETRIES = 1;
750: var BLOB_TIMEOUT = 3e4;
751: var TAGS_PAGE_SIZE = 100;
752: var OCI_MANIFEST_TYPE = "application/vnd.oci.image.manifest.v1+json";
753: function isRetryableError(error) {
754: if (error.name === "TimeoutError" || error.name === "AbortError") {
755: return true;
756: }
757: const msg = error.message.toLowerCase();
758: return msg.includes("timeout") || msg.includes("econnreset") || msg.includes("econnrefused") || msg.includes("network") || msg.includes("fetch failed");
759: }
760: function buildSignal(timeout, externalSignal) {
761: const timeoutSignal = AbortSignal.timeout(timeout);
762: return externalSignal ? AbortSignal.any([timeoutSignal, externalSignal]) : timeoutSignal;
763: }
764: ...
<path>/home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/toolkit-bridge-upgrade/packages/cli/node_modules/binpatch/dist/index.js</path>
<type>file</type>
<content>
1127: totalSize: downloadedSize,
1128: expectedSha256: validation.expectedSha256,
1129: steps
1130: };
1131: }
1132: return {
1133: async resolveChain(currentVersion, targetVersion, signal, report) {
1134: try {
1135: const token = await i("ghcr-token", () => client.getAnonymousToken(signal));
1136: const [targetManifest, patchTags] = await Promise.all([
1137: i(
1138: "fetch-target-manifest",
1139: () => client.fetchManifest(token, targetTag(targetVersion), signal)
1140: ),
1141: i(
1142: "list-patch-tags",
1143: () => client.listTags(token, PATCH_TAG_PREFIX, signal)
1144: )
1145: ]);
1146: const gzLayer = targetManifest.layers.find(
1147: ...
<path>/home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/toolkit-bridge-upgrade/packages/cli/test/e2e/delta-upgrade.test.ts</path> <type>file</type> <content> 1: /** 2: * E2E Test: Delta Upgrade via Binary Patching 3: * 4: * Downloads two consecutive stable release binaries from GitHub, 5: * generates a TRDIFF10 patch using zig-bsdiff, and verifies that 6: * our `applyPatch()` produces byte-identical output. 7: * 8: * Requires: zig-bsdiff binary available at /tmp/bsdiff (or via ZIG_BSDIFF_PATH env). 9: * Skipped in CI unless ZIG_BSDIFF_PATH is set. ...
<path>/home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/toolkit-bridge-upgrade/packages/cli/src/lib/upgrade.ts</path> <type>file</type> <content> 80: 81: /** 82: * How the current upgrade reached the offline code path. 83: * 84: * - `false` — online upgrade (network available) 85: * - `"explicit"` — user passed `--offline` flag 86: * - `"network-fallback"` — network failed, auto-fell back to cache 87: */ 88: export type OfflineMode = false | "explicit" | "network-fallback"; 89: 90: // Constants 91: 92: /** The git tag used for the rolling nightly GitHub release (stable fallback only). */ 93: export const NIGHTLY_TAG = "nightly"; 94: 95: /** npm registry base URL */ 96: const NPM_REGISTRY_URL = "https://registry.npmjs.org/sentry"; 97: 98: /** Regex to strip 'v' prefix from version strings */ 99: export const VERSION_PREFIX_REGEX = /^v/; 100: 101: /** GitHub pagination link for the next page. ...
5ffa5cd0fe02c0a5dc5bec9a8a099f4135c0b447 M packages/cli/src/lib/upgrade.ts M packages/cli/test/lib/upgrade.test.ts cae154a87ed717caf66b0bb607b821a8f11b29b9f30ef41a6faef87a4208527a - f35613506a5ef270c2075bf7ac0ae32a35edfb8c 376eb6f761021afc879be4bdd94a27696d734866
diff --git a/packages/cli/src/lib/upgrade.ts b/packages/cli/src/lib/upgrade.ts
index 376eb6f76..650f39f28 100644
--- a/packages/cli/src/lib/upgrade.ts
+++ b/packages/cli/src/lib/upgrade.ts
@@ -101,6 +101,12 @@ export const VERSION_PREFIX_REGEX = /^v/;
/** GitHub pagination link for the next page. */
const NEXT_PAGE_LINK_REGEX = /<([^>]+)>;\s*rel="next"/;
+/** Canonical GitHub REST repository release-list path. */
+const CANONICAL_RELEASES_PATH_REGEX = /^\/repositories\/\d+\/releases$/;
+
+/** Positive GitHub pagination page number. */
+const PAGE_NUMBER_REGEX = /^[1-9]\d*$/;
+
/** A resolved standalone-binary version and the source that must serve it. */
export type ResolvedUpgradeVersion = {
/** Version without a source-specific tag prefix. */
@@ -139,17 +145,26 @@ function getNextGitHubReleasePage(
return;
}
const url = new URL(match[1]);
+ const isSelectedSourcePath =
+ url.pathname === `/repos/${source.githubRepo}/releases`;
+ const ...
5ffa5cd0fe02c0a5dc5bec9a8a099f4135c0b447:packages/cli/src/lib/upgrade.ts:145: url.pathname !== `/repos/${source.githubRepo}/releases`
5ffa5cd0fe02c0a5dc5bec9a8a099f4135c0b447:packages/cli/test/lib/upgrade.test.ts:214: test("follows Toolkit release pagination to find the latest CLI release", async () => {
5ffa5cd0fe02c0a5dc5bec9a8a099f4135c0b447:packages/cli/test/lib/upgrade.test.ts:228: Link: '<https://api.github.com/repos/getsentry/toolkit/releases?per_page=100&page=2>; rel="next"',
5ffa5cd0fe02c0a5dc5bec9a8a099f4135c0b447:packages/cli/src/lib/delta-upgrade.ts:153: tag_name: release.tag_name.slice(source.tagPrefix.length),
5ffa5cd0fe02c0a5dc5bec9a8a099f4135c0b447:packages/cli/src/lib/delta-upgrade.ts:236: tag_name: release.tag_name.slice(source.tagPrefix.length),
5ffa5cd0fe02c0a5dc5bec9a8a099f4135c0b447:packages/cli/src/lib/release-notes.ts:437: if (!tagName.startsWith(source.tagPrefix)) {
5ffa5cd0fe02c0a5dc5bec9a8a099f4135c0b447:packages/cli/src/lib/release-notes.ts:633: prefetchedReleases ?? (await fetchReleasesForChangelog(source));
5ffa5cd0fe02c0a5dc5bec9a8a099f4135c0b447:packages/cli/src/lib/upgrade.ts:141: const url = new URL(match[1]);
5ffa5cd0fe02c0a5dc5bec9a8a099f4135c0b447:packages/cli/src/commands/cli/upgrade.ts:226: !(error instanceof UpgradeError && error.reason === "network_error")
At exact head 5ffa5cd0fe02c0a5dc5bec9a8a099f4135c0b447, pagination only accepts the owner/name path:
/home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/toolkit-bridge-upgrade/packages/cli/src/lib/upgrade.ts:145GitHub commonly emits canonical REST links such as:
https://api.github.com/repositories/<repository-id>/releases?page=2
The implementation rejects that valid path as an invalid pagination URL. Therefore, when Toolkit’s first 100 releases contain no CLI release, normal GitHub pagination can fail instead of reaching the next page. The test masks this by constructing a noncanonical /repos/getsentry/toolkit/releases Link:
/home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/toolkit-bridge-upgrade/packages/cli/test/lib/upgrade.test.ts:214/home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/toolkit-bridge-upgrade/packages/cli/test/lib/upgrade.test.ts:228Fix: Accept GitHub’s canonical /repositories/<numeric-id>/releases path, extract only a positive decimal page, and reconstruct the request from the selected source’s trusted base URL. Never fetch the Link URL verbatim.
Deterministic regression: Return 100 MCP releases on page one with:
Link: <https://api.github.com/repositories/1114546946/releases?per_page=100&page=2>; rel="next"
Return cli@1.2.3 on page two. Assert resolution returns 1.2.3 and the second request uses the trusted Toolkit release URL with page=2. This fails at the exact head.
The exact head constructs a URL without guarding parser failure:
/home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/toolkit-bridge-upgrade/packages/cli/src/lib/upgrade.ts:141A hostile value such as:
Link: <https://[invalid>; ...