Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -633,6 +633,9 @@ jobs:
with:
bun-version: "1.3.13"
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v6
with:
node-version: "22"
- uses: actions/cache@v5
id: cache
with:
Expand Down
59 changes: 50 additions & 9 deletions .lore.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ Credentials are stored in `~/.sentry/` with restricted permissions (mode 600).
## Library Usage

<!-- GENERATED:START library-prereq -->
Use Sentry CLI programmatically in Node.js (≥22.12) or Bun without spawning a subprocess:
Use Sentry CLI programmatically in Node.js (≥22.15) or Bun without spawning a subprocess:
<!-- GENERATED:END library-prereq -->

```typescript
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@
},
"description": "Sentry CLI - A command-line interface for using Sentry built by robots and humans for robots and humans",
"engines": {
"node": ">=22.12"
"node": ">=22.15"
},
"files": [
"dist/bin.cjs",
Expand Down
2 changes: 1 addition & 1 deletion script/bundle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@ const result = await build({
// Write the CLI bin wrapper (tiny — shebang + version check + dispatch).
// Version floor must track `engines.node` in package.json.
const BIN_WRAPPER = `#!/usr/bin/env node
{let v=process.versions.node.split(".").map(Number);if(v[0]<22||(v[0]===22&&v[1]<12)){console.error("Error: sentry requires Node.js 22.12 or later (found "+process.version+").\\n\\nEither upgrade Node.js, or install the standalone binary instead:\\n curl -fsSL https://cli.sentry.dev/install | bash\\n");process.exit(1)}}
{let v=process.versions.node.split(".").map(Number);if(v[0]<22||(v[0]===22&&v[1]<15)){console.error("Error: sentry requires Node.js 22.15 or later (found "+process.version+").\\n\\nEither upgrade Node.js, or install the standalone binary instead:\\n curl -fsSL https://cli.sentry.dev/install | bash\\n");process.exit(1)}}
{let e=process.emit;process.emit=function(n,...a){return n==="warning"?!1:e.apply(this,[n,...a])}}
require('./index.cjs')._cli().catch(()=>{process.exitCode=1});
`;
Expand Down
4 changes: 2 additions & 2 deletions src/commands/cli/upgrade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -431,9 +431,9 @@ async function spawnWithRetry(
stdio: ["ignore", "inherit", "inherit"],
env,
});
return await new Promise<number>((resolve) => {
return await new Promise<number>((resolve, reject) => {
proc.on("close", (code) => resolve(code ?? 1));
proc.on("error", () => resolve(1));
proc.on("error", (err) => reject(err));
});
} catch (error) {
// Translate the opaque Bun "Executable not found" error into an
Expand Down
29 changes: 9 additions & 20 deletions src/lib/api/sourcemaps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,11 @@ import { open, readFile, stat, unlink } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { promisify } from "node:util";
// biome-ignore lint/performance/noNamespaceImport: needed for feature-detected zstd access
import * as zlib from "node:zlib";
import {
gzip as gzipCb,
constants as zlibConstants,
zstdCompress as zstdCompressCb,
} from "node:zlib";
import pLimit from "p-limit";
import { z } from "zod";
import { ApiError } from "../errors.js";
Expand All @@ -35,22 +38,8 @@ import { getSdkConfig } from "../sentry-client.js";
import { type ZipCompression, ZipWriter } from "../sourcemap/zip.js";
import { apiRequestToRegion } from "./infrastructure.js";

const gzipAsync = promisify(zlib.gzip);
// zstdCompress is available in Node 22.15+. Feature-detect to avoid crashing
// the npm bundle on older Node versions (e.g., CI runners with Node 20).
// zstdCompress is available in Node 22.15+. Feature-detect to avoid crashing
// the npm bundle on older Node versions (e.g., CI runners with Node 20).
// biome-ignore lint/suspicious/noExplicitAny: zstd types unavailable on older @types/node
const zstdCompressFn = (zlib as any).zstdCompress as
| ((...args: unknown[]) => unknown)
| undefined;
const zstdCompressAsync =
typeof zstdCompressFn === "function"
? (promisify(zstdCompressFn) as (
buf: Buffer,
opts?: unknown
) => Promise<Buffer>)
: undefined;
const gzipAsync = promisify(gzipCb);
const zstdCompressAsync = promisify(zstdCompressCb);
const log = logger.withTag("api.sourcemaps");

// ── Schemas ─────────────────────────────────────────────────────────
Expand Down Expand Up @@ -219,13 +208,13 @@ export async function encodeChunk(
buf: Buffer,
encoding: UploadEncoding | undefined
): Promise<Uint8Array> {
if (encoding === "zstd" && zstdCompressAsync) {
if (encoding === "zstd") {
// L3 is libzstd's default; passed explicitly for self-documenting
// code. L9+ trades ~14% size for 4x compress time and forces the
// server's decoder to allocate 15-30 MiB of window state -- not
// worth it once decode cost is counted.
return await zstdCompressAsync(buf, {
params: { [zlib.constants.ZSTD_c_compressionLevel]: 3 },
params: { [zlibConstants.ZSTD_c_compressionLevel]: 3 },
});
}
if (encoding === "gzip") {
Expand Down
6 changes: 5 additions & 1 deletion src/lib/db/sqlite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,11 @@ export class Database {
this.db.exec("COMMIT");
return result;
} catch (error) {
this.db.exec("ROLLBACK");
try {
this.db.exec("ROLLBACK");
} catch (rollbackError) {
log.debug("ROLLBACK failed after transaction error", rollbackError);
}
throw error;
}
};
Expand Down
9 changes: 8 additions & 1 deletion src/lib/delta-upgrade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { unlinkSync } from "node:fs";

// biome-ignore lint/performance/noNamespaceImport: Sentry SDK recommends namespace import
import * as Sentry from "@sentry/node-core/light";
import { compare as semverCompare } from "semver";
import { compare as semverCompare, valid as semverValid } from "semver";

import {
GITHUB_RELEASES_URL,
Expand Down Expand Up @@ -460,10 +460,17 @@ export function filterAndSortChainTags(
currentVersion: string,
targetVersion: string
): string[] {
if (!(semverValid(currentVersion) && semverValid(targetVersion))) {
return [];
}

const chainTags: { tag: string; version: string }[] = [];

for (const tag of allTags) {
const version = tag.slice(PATCH_TAG_PREFIX.length);
if (!semverValid(version)) {
continue;
}
// Include tags where: currentVersion < version <= targetVersion
if (
semverCompare(version, currentVersion) === 1 &&
Expand Down
13 changes: 11 additions & 2 deletions src/lib/shell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,11 @@
import { existsSync } from "node:fs";
import { access, readFile, writeFile } from "node:fs/promises";
import { basename, delimiter, join } from "node:path";
import { logger } from "./logger.js";
import { whichSync } from "./which.js";

const log = logger.withTag("shell");

/** Supported shell types */
export type ShellType = "bash" | "zsh" | "fish" | "sh" | "ash" | "unknown";

Expand Down Expand Up @@ -299,7 +302,12 @@ export async function addToGitHubPath(
let content = "";
try {
content = await readFile(env.GITHUB_PATH, "utf-8");
} catch {
} catch (error) {
const code = (error as NodeJS.ErrnoException).code;
if (code !== "ENOENT") {
log.debug(`Failed to read GITHUB_PATH (${code}):`, error);
Comment thread
BYK marked this conversation as resolved.
return false;
}
// File doesn't exist yet — start with empty content
Comment thread
BYK marked this conversation as resolved.
}

Expand All @@ -310,7 +318,8 @@ export async function addToGitHubPath(
await writeFile(env.GITHUB_PATH, newContent, "utf-8");
}
return true;
} catch {
} catch (error) {
log.debug("Failed to update GITHUB_PATH:", error);
return false;
}
}
Expand Down
31 changes: 10 additions & 21 deletions src/lib/telemetry/zstd-transport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,11 @@ import * as http from "node:http";
import * as https from "node:https";
import { Readable } from "node:stream";
import { promisify } from "node:util";
// biome-ignore lint/performance/noNamespaceImport: needed for feature-detected zstd access
import * as zlib from "node:zlib";
import {
gzip as gzipCb,
constants as zlibConstants,
zstdCompress as zstdCompressCb,
} from "node:zlib";
import {
createTransport,
suppressTracing,
Expand Down Expand Up @@ -78,22 +81,8 @@ const ZSTD_THRESHOLD = 1024;
*/
const GZIP_THRESHOLD = 1024 * 32;

const gzipAsync = promisify(zlib.gzip);
// zstdCompress is available in Node 22.15+. Feature-detect to avoid crashing
// the npm bundle on older Node versions (e.g., CI runners with Node 20).
// zstdCompress is available in Node 22.15+. Feature-detect to avoid crashing
// the npm bundle on older Node versions (e.g., CI runners with Node 20).
// biome-ignore lint/suspicious/noExplicitAny: zstd types unavailable on older @types/node
const zstdCompressFn = (zlib as any).zstdCompress as
| ((...args: unknown[]) => unknown)
| undefined;
const zstdCompressAsync =
typeof zstdCompressFn === "function"
? (promisify(zstdCompressFn) as (
buf: Buffer,
opts?: unknown
) => Promise<Buffer>)
: undefined;
const gzipAsync = promisify(gzipCb);
const zstdCompressAsync = promisify(zstdCompressCb);

/**
* Factory for the SDK's `Sentry.init({ transport })` option.
Expand Down Expand Up @@ -288,9 +277,9 @@ export async function maybeCompress(
return { payload: buf, encodingApplied: "none" };
}

if (encoding === "zstd" && zstdCompressAsync) {
if (encoding === "zstd") {
const out = await zstdCompressAsync(buf, {
params: { [zlib.constants.ZSTD_c_compressionLevel]: ZSTD_LEVEL },
params: { [zlibConstants.ZSTD_c_compressionLevel]: ZSTD_LEVEL },
});
return {
payload: Buffer.from(out.buffer, out.byteOffset, out.byteLength),
Expand All @@ -304,7 +293,7 @@ export async function maybeCompress(

/** Feature-detect zstd support on the current runtime. */
export function hasZstdSupport(): boolean {
return zstdCompressAsync !== undefined;
return typeof zstdCompressCb === "function";
}

/**
Expand Down
19 changes: 18 additions & 1 deletion src/lib/upgrade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -572,7 +572,24 @@ async function streamDecompressToFile(
if (writeError) {
break;
}
writer.write(chunk);
const ok = writer.write(chunk);
if (!(ok || writeError)) {
// Race drain against error — an I/O failure (ENOSPC) while the
// buffer is full would never emit 'drain', causing a hang.
Comment thread
BYK marked this conversation as resolved.
// Clean up the unused listener to avoid MaxListenersExceededWarning.
await new Promise<void>((resolve) => {
const onDrain = (): void => {
writer.removeListener("error", onError);
resolve();
};
const onError = (): void => {
writer.removeListener("drain", onDrain);
resolve();
};
writer.once("drain", onDrain);
writer.once("error", onError);
});
Comment thread
BYK marked this conversation as resolved.
}
Comment thread
cursor[bot] marked this conversation as resolved.
}
} finally {
await new Promise<void>((resolve, reject) => {
Expand Down
5 changes: 4 additions & 1 deletion src/lib/which.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@

import { execFileSync } from "node:child_process";

/** Matches CRLF or LF line endings. Used to split `where.exe` output on Windows. */
const NEWLINE_RE = /\r?\n/;

/**
* Synchronously find the full path to a command in the system PATH.
*
Expand Down Expand Up @@ -58,7 +61,7 @@ export function whichSync(
);
}

return stdout.trim().split("\n")[0] || null;
return stdout.trim().split(NEWLINE_RE)[0] || null;
} catch {
return null;
}
Expand Down
Loading