Dashboard › cli › Session 01dbUUgGctdT
01dbUUgGctdTeDmFpREAD-ONLY security/supply-chain 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 and inspect all changed files plus relevant tests. Focus on genuine typed HTTP-404-only fallback; fail-closed 401/403/429/5xx/transport/malformed data; pagination URL origin/path and cycle checks; one-source affinity; GHCR token scope/redirect credential stripping; untrusted tags/manifests/URLs; SHA-256/delta integrity; cancellation/cache/offline boundaries; installer unchanged scope. The GitHub repository probe before GHCR nightly access is intentional because GHCR missing-package token responses are ambiguous 401/403. The verified delta cache is intentionally version-pair keyed because dual-published artifacts must be byte-identical and final digest is enforced; identify only concrete integrity bypasses. 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.
No files found
<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. ...
<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: <!-- ...
/home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/toolkit-bridge-upgrade/packages/cli/CONTRIBUTING.md
<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 ...
<path>/home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/toolkit-bridge-upgrade/packages/cli/CONTRIBUTING.md</path> <type>file</type> <content> 1: # Contributing to Sentry CLI 2: 3: This guide documents the patterns and conventions used in this CLI for consistency. 4: 5: ## Command Patterns 6: 7: We follow [gh CLI](https://cli.github.com/) conventions for best-in-class developer experience. 8: 9: ### List Commands 10: 11: List commands use **optional positional arguments** for context with smart auto-detection. 12: 13: ```bash 14: sentry org list [--limit N] [--json] 15: sentry project list [org] [--limit N] [--json] 16: sentry issue list [<org>/<project>] [--json] 17: ``` 18: 19: **Target syntax**: 20: - `<org>/<project>` - Explicit organization and project (e.g., `my-org/frontend`) 21: - `<org>/` - All projects in the specified organization 22: - `<project>` - Search for project by name across all accessible organizations 23: - *(omit)* - ...
5ffa5cd0fe02c0a5dc5bec9a8a099f4135c0b447 ec83887a16f780f32fba4b7d710bad262dba3a22 5ffa5cd0fe02c0a5dc5bec9a8a099f4135c0b447 cae154a87ed717caf66b0bb607b821a8f11b29b9f30ef41a6faef87a4208527a - 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 +++++++- ...
...output truncated...
Full output saved to: /home/byk/.local/share/opencode/tool-output/tool_088f960f5001jLo5kEr7itT0zV
+export function getGitHubReleasesUrl(
+ source: UpgradeSource = PRIMARY_UPGRADE_SOURCE
+): string {
+ return `https://api.github.com/repos/${source.githubRepo}/releases`;
+}
+
+/** Build the GitHub API URL for one source-specific release tag. */
+export function getGitHubReleaseByTagUrl(
+ version: string,
+ source: UpgradeSource = PRIMARY_UPGRADE_SOURCE
+): string {
+ const tag = `${source.tagPrefix}${version}`;
+ return `${getGitHubReleasesUrl(source)}/tags/${encodeURIComponent(tag)}`;
+}
+
+/** Build the GitHub API URL used to discover a source's latest CLI release. */
+export function getGitHubLatestReleaseUrl(
+ source: UpgradeSource = PRIMARY_UPGRADE_SOURCE
+): string {
+ return source.tagPrefix
+ ? `${getGitHubReleasesUrl(source)}?per_page=100`
+ : `${getGitHubReleasesUrl(source)}/latest`;
}
-/** GitHub API base URL for releases */
-export ...
<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/src/lib/ghcr.ts</path>
<type>file</type>
<content>
495: export async function listTags(
496: token: string,
497: prefix?: string,
498: signal?: AbortSignal,
499: source: UpgradeSource = PRIMARY_UPGRADE_SOURCE
500: ): Promise<string[]> {
501: const allTags: string[] = [];
502: let lastTag: string | undefined;
503:
504: for (;;) {
505: const tags = await fetchTagPage(token, lastTag, signal, source);
506: if (tags.length === 0) {
507: break;
508: }
509:
510: for (const tag of tags) {
511: if (!prefix || tag.startsWith(prefix)) {
512: allTags.push(tag);
513: }
514: }
515:
516: if (tags.length < TAGS_PAGE_SIZE) {
517: break;
518: }
519:
520: lastTag = tags.at(-1);
521: }
522:
523: return allTags;
524: }
525:
526: /**
527: * Download an OCI layer blob as an ArrayBuffer. ...
<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: ...
No files found
No files found
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/src/commands/cli/upgrade.ts</path> <type>file</type> <content> 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. ...
<path>/home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/toolkit-bridge-upgrade/packages/cli/src/commands/cli/upgrade.ts</path>
<type>file</type>
<content>
700: // the same lock path (ppid takeover), this is a harmless no-op.
701: const currentInstallDir = dirname(getCurlInstallPaths().installPath);
702: const installDir = resolveUpgradeInstallDir(currentInstallDir, pathEnv);
703: try {
704: await runSetupOnNewBinary({
705: binaryPath: downloadResult.tempBinaryPath,
706: method,
707: channel,
708: install: true,
709: installDir,
710: ensureAuthScopes: !json,
711: noAgentSkills,
712: });
713: } finally {
714: releaseLock(downloadResult.lockPath);
715: }
716: } else {
717: // Package managers replace their PATH entry in place. Resolve it after the
718: // install so setup runs with the new CLI, not Node's process.execPath or a
719: // removed ...
<path>/home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/toolkit-bridge-upgrade/packages/cli/src/lib/release-notes.ts</path> <type>file</type> <content> 1: /** 2: * Release Notes Parser & Aggregation 3: * 4: * Extracts user-facing changelog entries from GitHub Release bodies (stable) 5: * or conventional commit messages (nightly). Uses `marked.lexer()` for 6: * AST-based section extraction and produces structured data that can be 7: * re-serialized as filtered markdown for rendering via `renderMarkdown()`. ...
<path>/home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/toolkit-bridge-upgrade/packages/cli/src/lib/release-notes.ts</path>
<type>file</type>
<content>
640: });
641: }
642:
643: /**
644: * Build a changelog summary from nightly commit data.
645: *
646: * Separated from the fetch logic for testability.
647: */
648: function buildNightlyChangelogSummary(
649: commits: GitHubCommit[],
650: fromVersion: string,
651: toVersion: string,
652: maxItems?: number
653: ): ChangelogSummary | null {
654: const sections = parseCommitMessages(commits);
655: return buildSummaryFromSections(sections, fromVersion, toVersion, maxItems);
656: }
657:
658: /**
659: * Fetch changelog for a nightly upgrade using the GitHub Commits API.
660: *
661: * Nightly versions encode the commit timestamp as unix seconds in the
662: * pre-release identifier (`X.Y.Z-dev.<unix-seconds>`). This enables
663: * timestamp-based commit listing without git tags. ...
<path>/home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/toolkit-bridge-upgrade/packages/cli/src/lib/version-check.ts</path>
<type>file</type>
<content>
1: /**
2: * Background version check for "new version available" notifications.
3: *
4: * For nightly builds (CLI_VERSION contains "-dev.<timestamp>"), checks GHCR for the
5: * latest nightly version via the OCI manifest annotation. For stable builds,
6: * checks GitHub Releases. Results are cached in the database and shown on
7: * subsequent runs.
8: */
9:
10: // biome-ignore lint/performance/noNamespaceImport: Sentry SDK recommends namespace import
11: import * as Sentry from "@sentry/node-core/light";
12: import { compare as semverCompare } from "semver";
13: import type { UpgradeSource } from "./binary.js";
14: import { CLI_VERSION } from "./constants.js";
15: import { getReleaseChannel } from "./db/release-channel.js";
16: import {
17: getVersionCheckInfo,
18: markUpdateNotified,
19: ...
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
@@ -20,34 +20,38 @@ import { afterEach, beforeEach, describe, expect, test } from "vitest";
import {
acquireLock,
compareVersions,
determineInstallDir,
fetchWithUpgradeError,
getBinaryDownloadUrl,
getBinaryFilename,
getBinaryPaths,
+ getGitHubReleaseByTagUrl,
getLegacyInstallDirs,
getPlatformBinaryName,
installBinary,
isDowngrade,
isMusl,
releaseLock,
replaceBinarySync,
+ resolveUpgradeSource,
samePath,
+ UPGRADE_SOURCES,
+ UpgradeSourceNotFoundError,
} from "../../src/lib/binary.js";
import { UpgradeError } from "../../src/lib/errors.js";
describe("getBinaryDownloadUrl", () => {
test("builds correct URL for current platform", () => {
const url = getBinaryDownloadUrl("1.0.0");
- ...
diff --git a/packages/cli/test/lib/delta-upgrade.mocked.test.ts b/packages/cli/test/lib/delta-upgrade.mocked.test.ts
index cd0a2fddc..f6186ab59 100644
--- a/packages/cli/test/lib/delta-upgrade.mocked.test.ts
+++ b/packages/cli/test/lib/delta-upgrade.mocked.test.ts
@@ -94,20 +94,20 @@ describe("resolveStableDelta", () => {
// Create a "current binary" to patch from
const oldBinaryPath = tempFile("old-binary.bin");
const destPath = tempFile("patched-binary.bin");
writeFileSync(oldBinaryPath, Buffer.from("old binary content for testing"));
// Set up fetch mocks — releases API + patch download
// Since applyPatch will fail (we don't have a real TRDIFF10 matching this binary),
// we expect resolveStableDelta to throw, but the chain resolution should succeed
- const patchUrl = `https://github.com/getsentry/cli/releases/download/0.14.0/${BINARY_NAME}.patch`;
+ const patchUrl = ...
diff --git a/packages/cli/test/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
@@ -119,17 +119,17 @@ describe("upgradeCommand.func", () => {
// Note: We skip testing "unknown installation method" case because
// detectInstallationMethod() runs actual shell commands (npm list, etc.)
// which can be slow/flaky in CI. The unknown method handling is tested
// indirectly through the upgrade.ts unit tests in lib/upgrade.test.ts.
test("shows installation info with specified method", async () => {
globalThis.fetch = (async () =>
- new Response(JSON.stringify({ tag_name: "v0.0.0-dev" }), {
+ new Response(JSON.stringify([{ tag_name: "cli@0.0.0-dev" }]), {
status: 200,
headers: { "Content-Type": "application/json" },
})) as typeof fetch;
const func = await upgradeCommand.loader();
const { ...
/**
* 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.
* Using `fetch` with `redirect: "follow"` would forward the Authorization
* header to Azure, which returns 404. Must follow the redirect manually
* without the auth header.
*/
import { getUserAgent } from "./constants.js";
import { customFetch } from ...
Found 13 matches /home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/toolkit-bridge-upgrade/packages/cli/test/lib/ghcr.test.ts: Line 92: const token = await getAnonymousToken(); Line 104: await expect(getAnonymousToken(UPGRADE_SOURCES[0])).resolves.toBe( Line 112: await expect(getAnonymousToken()).rejects.toThrow(UpgradeError); Line 113: await expect(getAnonymousToken()).rejects.toThrow( Line 123: await expect(getAnonymousToken()).rejects.toThrow(UpgradeError); Line 124: await expect(getAnonymousToken()).rejects.toThrow( Line 143: const request = getAnonymousToken(undefined, controller.signal); Line 159: await expect(getAnonymousToken()).rejects.toThrow(UpgradeError); Line 160: await expect(getAnonymousToken()).rejects.toThrow( /home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/toolkit-bridge-upgrade/packages/cli/src/lib/upgrade.ts: Line 597: const token ...
<path>/home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/toolkit-bridge-upgrade/packages/cli/src/lib/upgrade.ts</path>
<type>file</type>
<content>
520: )
521: ).version;
522: }
523:
524: /**
525: * Fetch the latest version from npm registry.
526: *
527: * @returns Latest version string
528: * @throws {UpgradeError} When fetch fails or response is invalid
529: */
530: export async function fetchLatestFromNpm(): Promise<string> {
531: const response = await fetchWithUpgradeError(
532: `${NPM_REGISTRY_URL}/latest`,
533: { headers: { Accept: "application/json" } },
534: "npm registry"
535: );
536:
537: if (!response.ok) {
538: throw new UpgradeError(
539: "network_error",
540: `Failed to fetch from npm: ${response.status}`
541: );
542: }
543:
544: const data = (await response.json()) as { version?: string };
545:
546: if (!data.version) {
547: throw new UpgradeError("network_error", "No version ...
import { createHash } from 'crypto';
import { copyFileSync, constants, openSync, writeSync, closeSync } from 'fs';
import { open, unlink, readFile, readdir, mkdir, writeFile } from 'fs/promises';
import { tmpdir } from 'os';
import { join } from 'path';
import { Readable } from 'stream';
import { zstdDecompressSync, createZstdDecompress } from 'zlib';
// src/bspatch.ts
var TRDIFF10_MAGIC = "TRDIFF10";
var HEADER_SIZE = 32;
var MAX_OUTPUT_SIZE = 2 * 1024 * 1024 * 1024;
function offtin(buf, offset) {
const view = new DataView(buf.buffer, buf.byteOffset + offset, 8);
const lo = view.getUint32(0, true);
const hi = view.getUint32(4, true);
const magnitude = hi % 2147483648 * 4294967296 + lo;
if (magnitude !== 0 && hi >= 2147483648) {
return -magnitude;
}
return magnitude;
}
function parsePatchHeader(patch) {
if (patch.byteLength < HEADER_SIZE) {
throw new Error(
`Patch too small: ${patch.byteLength} bytes (need at least ${HEADER_SIZE})`
);
}
const ...
diff --git a/packages/cli/src/lib/binary.ts b/packages/cli/src/lib/binary.ts
index 75582612a..61e10568d 100644
--- a/packages/cli/src/lib/binary.ts
+++ b/packages/cli/src/lib/binary.ts
@@ -93,24 +93,51 @@ export function getLegacyInstallDirs(homeDir: string): string[] {
* `db/install-info.ts` can import it without creating a circular
* dependency.
*/
export type InstallationMethod =
| "curl"
| "brew"
| "npm"
| "pnpm"
| "bun"
| "yarn"
| "unknown";
+/** A repository pair that hosts CLI stable releases and nightly OCI images. */
+export type UpgradeSource = {
+ /** GitHub `owner/repository` containing CLI release assets. */
+ readonly githubRepo: string;
+ /** GHCR `owner/package` containing CLI nightly images and delta patches. */
+ readonly ghcrRepo: string;
+ /** Prefix attached to CLI release tags in this repository. */
+ readonly tagPrefix: string;
+};
+
+/** Ordered CLI release sources. The resolver falls through only on HTTP 404. ...
Found 100 matches (more matches available)
/home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/toolkit-bridge-upgrade/packages/cli/test/lib/upgrade.test.ts:
Line 214: test("follows Toolkit release pagination to find the latest CLI release", async () => {
Line 245: test("rejects GitHub release pagination outside the selected source", async () => {
Line 258: "GitHub returned an invalid release pagination URL"
Line 263: test("rejects cyclic GitHub release pagination", async () => {
Line 276: "GitHub returned cyclic release pagination"
Line 493: test("creates error with default message for offline_cache_miss", () => {
Line 494: const error = new UpgradeError("offline_cache_miss");
Line 495: expect(error.reason).toBe("offline_cache_miss");
Line 497: "Cannot upgrade offline — no pre-downloaded update is available."
Line 1904: test("aborts early if signal is already aborted", async () => {
Line ...
<path>/home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/toolkit-bridge-upgrade/packages/cli/test/lib/upgrade.test.ts</path>
<type>file</type>
<content>
1880: );
1881: });
1882:
1883: test("throws UpgradeError when manifest has no version annotation", async () => {
1884: mockFetch(async (url) => {
1885: const urlStr = String(url);
1886: if (urlStr === "https://api.github.com/repos/getsentry/toolkit") {
1887: return new Response(null, { status: 200 });
1888: }
1889: if (urlStr.includes("ghcr.io/token")) {
1890: return new Response(JSON.stringify({ token: "tok" }), { status: 200 });
1891: }
1892: return new Response(
1893: JSON.stringify({ schemaVersion: 2, layers: [], annotations: {} }),
1894: { status: 200 }
1895: );
1896: });
1897:
1898: await expect(fetchLatestNightlyVersion()).rejects.toThrow(UpgradeError);
1899: await ...
Found 23 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 1204: "https://github.com/getsentry/toolkit/releases/download/"
/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 () => {
/home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/toolkit-bridge-upgrade/packages/cli/test/lib/delta-upgrade.test.ts:
Line 951: const ...
<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. ...
5ffa5cd0fe02c0a5dc5bec9a8a099f4135c0b447 M packages/cli/src/lib/upgrade.ts M packages/cli/test/lib/upgrade.test.ts cae154a87ed717caf66b0bb607b821a8f11b29b9f30ef41a6faef87a4208527a -
diff --git a/packages/cli/src/lib/upgrade.ts b/packages/cli/src/lib/upgrade.ts
index 376eb6f76..f35613506 100644
--- a/packages/cli/src/lib/upgrade.ts
+++ b/packages/cli/src/lib/upgrade.ts
@@ -139,17 +139,26 @@ function getNextGitHubReleasePage(
return;
}
const url = new URL(match[1]);
+ const isSelectedSourcePath =
+ url.pathname === `/repos/${source.githubRepo}/releases`;
+ const isCanonicalRepositoryPath =
+ /^\/repositories\/\d+\/releases$/.test(url.pathname);
+ const page = url.searchParams.get("page");
if (
url.protocol !== "https:" ||
url.hostname !== "api.github.com" ||
- url.pathname !== `/repos/${source.githubRepo}/releases`
+ !(isSelectedSourcePath || isCanonicalRepositoryPath) ||
+ page === null ||
+ !/^[1-9]\d*$/.test(page)
) {
throw new UpgradeError(
"network_error",
"GitHub returned an invalid release pagination URL"
);
}
- return url.href;
+ const nextPage = new ...
723375b6ce (Burak Yigit Kaya 2026-09-09 23:46:47 +0000 118) const releases = Array.isArray(data) ? data : [data]; 723375b6ce (Burak Yigit Kaya 2026-09-09 23:46:47 +0000 119) return releases 04867eaf9b (Burak Yigit Kaya 2026-09-09 13:42:44 +0000 120) .filter((release) => !(release.draft || release.prerelease)) 04867eaf9b (Burak Yigit Kaya 2026-09-09 13:42:44 +0000 121) .map((release) => release.tag_name) 04867eaf9b (Burak Yigit Kaya 2026-09-09 13:42:44 +0000 122) .filter( 04867eaf9b (Burak Yigit Kaya 2026-09-09 13:42:44 +0000 123) (tag): tag is string => 04867eaf9b (Burak Yigit Kaya 2026-09-09 13:42:44 +0000 124) typeof tag === "string" && tag.startsWith(source.tagPrefix) 04867eaf9b (Burak Yigit Kaya 2026-09-09 13:42:44 +0000 125) ) 723375b6ce (Burak Yigit Kaya 2026-09-09 23:46:47 +0000 126) .map((tag) => tag.slice(source.tagPrefix.length)) 723375b6ce (Burak Yigit Kaya 2026-09-09 23:46:47 +0000 127) .map((tag) => ...
GitHub’s canonical pagination URL is rejected.
At exact head 5ffa5cd0fe02c0a5dc5bec9a8a099f4135c0b447, /home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/toolkit-bridge-upgrade/packages/cli/src/lib/upgrade.ts:132-145 accepts only /repos/getsentry/toolkit/releases. GitHub may return canonical REST links such as /repositories/1114546946/releases. A Toolkit first page containing only non-CLI releases therefore fails instead of reaching the next page. The test at packages/cli/test/lib/upgrade.test.ts:214-243 uses the non-canonical path and misses this production response shape.
Impact: latest-version discovery can fail after migration despite valid later-page CLI releases. Curl, Homebrew, background checks, and stable upgrades are affected.
Fix: accept GitHub’s canonical repository-ID path, then rebuild the next request from the selected source URL using only a validated positive decimal page value. Never follow the returned canonical URL directly.
Deterministic regression: return:
Link: <https://api.github.com/repositories/1114546946/releases?per_page=100&page=2>; rel="next"
Assert that the second request is reconstructed as:
https://api.github.com/repos/getsentry/toolkit/releases?per_page=100&page=2
Also retain origin, path, malformed-page, and cycle rejection tests.
Initial verification was clean and matched the requested revisions:
ec83887a16f780f32fba4b7d710bad262dba3a225ffa5cd0fe02c0a5dc5bec9a8a099f4135c0b447cae154a87ed717caf66b0bb607b821a8f11b29b9f30ef41a6faef87a4208527aDuring final verification, the worktree changed outside this review:
packages/cli/src/lib/upgrade.tspackages/cli/test/lib/upgrade.test.tsThe uncommitted changes appear to address the pagination defect, but they are not part of the requested exact head and were not reviewed as an immutable ...