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
15 changes: 14 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -308,7 +308,7 @@ jobs:
# resolve correctly; workflow-level env evaluates before the job's
# environment: is applied.
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
# Set on main/release branches so build.ts runs binpunch + creates .gz
# Set on main/release branches so build.ts creates .gz archives
RELEASE_BUILD: ${{ github.event_name != 'pull_request' && '1' || '' }}
# Codesigning: only on main/release pushes (fork PRs lack secrets)
FOSSILIZE_SIGN: ${{ github.event_name == 'push' && (github.ref_name == 'main' || startsWith(github.ref_name, 'release/')) && 'y' || 'n' }}
Expand Down Expand Up @@ -350,6 +350,19 @@ jobs:
echo "$OUTPUT"
exit 1
fi
- name: Verify code signature (darwin)
# Only runs when signing was active (main/release pushes) and target is darwin.
# The smoke test alone does NOT catch invalid signatures — AMFI only SIGKILLs
# quarantined (downloaded) binaries, not freshly-built ones on the build machine.
if: >-
startsWith(matrix.target, 'darwin') &&
github.event_name == 'push' &&
(github.ref_name == 'main' || startsWith(github.ref_name, 'release/'))
shell: bash
run: |
BIN=./dist-bin/sentry-${{ matrix.target }}
echo "Verifying code signature on $BIN..."
rcodesign verify "$BIN"
- name: Upload binary artifact
uses: actions/upload-artifact@v7
with:
Expand Down
2 changes: 1 addition & 1 deletion .lore.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
* **Auth token env var override pattern: SENTRY\_AUTH\_TOKEN > SENTRY\_TOKEN > SQLite**: Auth token precedence in \`src/lib/db/auth.ts\`: \`SENTRY\_AUTH\_TOKEN\` > \`SENTRY\_TOKEN\` > SQLite OAuth token. \`getEnvToken()\` trims env vars (empty/whitespace = unset). \`AuthSource\` tracks provenance. \`ENV\_SOURCE\_PREFIX = "env:"\` — use \`.length\` not hardcoded 4. Env tokens bypass refresh/expiry. \`isEnvTokenActive()\` guards auth commands. Logout must NOT clear stored auth when env token active. \`runInteractiveLogin\` catches OAuth flow errors internally and returns falsy on failure; login command sets \`process.exitCode = 1\` and returns normally (does NOT reject). Tests expecting \`rejects.toThrow()\` will fail — assert via fetch-call inspection instead. \`requestDeviceCode\` requires \`SENTRY\_CLIENT\_ID\` env var.

<!-- lore:019e4f9d-b0e0-72b0-ab0f-1740206c9d80 -->
* **Binary build pipeline: esbuild → fossilize → Node SEA (replacing Bun.build compile)**: Binary build pipeline: \`src/bin.ts → \[esbuild CJS, node24 target] → dist-build/bin.js → \[fossilize --no-bundle] → Node SEA binary → \[binpunch ICU hole-punch] → gzip\`. Strip debug symbols handled INSIDE fossilize (as of fossilize 0.7.0) — fossilize strips the copied binary BEFORE postject injection. Strip MUST happen before injection — after SEA injection, \`strip\` fails ('section .text can't be allocated in segment 2'). macOS: \`strip -x\` on unsigned copy; cross-strip from Linux silently fails (caught). Windows: skipped (no debug symbols). NODE\_VERSION='lts'. ALL\_TARGETS: darwin-arm64, darwin-x64, linux-arm64, linux-x64, win32-x64 + musl variants. Post-process: rename \`sentry-win-x64.exe\`→\`sentry-windows-x64.exe\`. UPX RULED OUT — destroys ELF notes. \`FOSSILIZE\_SIGN=y\` on push to main/release. Gzip only when \`RELEASE\_BUILD=1\`. \`stripCachedNodeBinaries()\` removed from \`script/build.ts\` — superseded by fossilize 0.7.0.
* **Binary build pipeline: esbuild → fossilize → Node SEA (replacing Bun.build compile)**: Binary build pipeline: \`src/bin.ts → \[esbuild CJS, node24 target] → dist-build/bin.js → \[fossilize --no-bundle --hole-punch] → Node SEA binary → gzip\`. CRITICAL ORDER: hole-punch MUST happen BEFORE signing (issue #1033 — hole-punching after signing invalidates macOS code signature, causing AMFI SIGKILL). As of fossilize 0.8.0, hole-punch runs inside fossilize via \`--hole-punch\` flag (uses binpunch internally), between chmod and sign+notarize. Strip debug symbols also handled INSIDE fossilize (as of fossilize 0.7.0). macOS: \`strip -x\` on unsigned copy; cross-strip from Linux silently fails (caught). Windows: skipped. NODE\_VERSION='lts'. ALL\_TARGETS: darwin-arm64, darwin-x64, linux-arm64, linux-x64, win32-x64 + musl variants. Post-process: rename \`sentry-win-x64.exe\`→\`sentry-windows-x64.exe\`. UPX RULED OUT — destroys ELF notes. \`FOSSILIZE\_SIGN=y\` on push to main/release. Gzip only when \`RELEASE\_BUILD=1\`. binpunch is opt-in via \`--hole-punch\` flag / \`FOSSILIZE\_HOLE\_PUNCH\` env var — NOT default-on (removes non-English ICU locale data, breaking i18n for consumers). Cache stays pristine; all mutations (strip, inject, hole-punch, sign) happen on per-build output copy.

<!-- lore:019e56e2-f89c-7846-8953-1e47fb470cbf -->
* **Binary size breakdown: 94.5% is Node.js runtime — bundled code is ~6.3 MiB**: Binary composition (linux-x64, Node 24 LTS): Node.js runtime=121 MiB (ships with debug symbols). \`strip --strip-unneeded\` → 99 MiB (-17 MiB raw, -4 MiB compressed). Strip built into fossilize 0.7.0 — happens on the copied binary BEFORE postject injection. After strip+SEA+binpunch: ~108 MiB raw, ~30 MiB gzip (vs 125 MiB / 34 MiB unstripped). .rodata=52.5 MB: V8 snapshot ~12 MB, ICU full-icu data ~28 MB. UPX compresses to 25 MiB but DESTROYS ELF notes — ruled out. \`--with-intl=small-icu\` saves ~26-28 MiB (biggest win from custom build); \`--without-lief\` BREAKS SEA; \`--without-sqlite\` BREAKS CLI; \`--disable-single-executable-application\` BREAKS EVERYTHING. Custom build deferred — poor cost/benefit (~3.5h build vs 5min fossilize). Final vs Bun: download 30 MiB (Bun: 32 MiB), \`--version\` ~1.0s (Bun: ~1.9s), completions ~150ms (Bun: ~180ms).
Expand Down
10 changes: 2 additions & 8 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,12 @@
"@types/react": "^19.2.14",
"@types/semver": "^7.7.1",
"@vitest/coverage-v8": "^4.1.7",
"binpunch": "^1.0.0",
"chalk": "^5.6.2",
"cli-highlight": "^2.1.11",
"consola": "^3.4.2",
"esbuild": "^0.25.0",
"fast-check": "^4.5.3",
"fossilize": "^0.7.0",
"fossilize": "^0.8.0",
"hono": "^4.12.15",
"http-cache-semantics": "^4.2.0",
"ignore": "^7.0.5",
Expand Down Expand Up @@ -124,10 +123,5 @@
"check:stale-refs": "pnpm tsx script/check-stale-references.ts"
},
"type": "module",
"types": "./dist/index.d.cts",
"patchedDependencies": {
"@stricli/core@1.2.5": "patches/@stricli%2Fcore@1.2.5.patch",
"@sentry/node-core@10.50.0": "patches/@sentry%2Fnode-core@10.50.0.patch",
"@sentry/core@10.50.0": "patches/@sentry%2Fcore@10.50.0.patch"
}
"types": "./dist/index.d.cts"
}
14 changes: 6 additions & 8 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 6 additions & 12 deletions script/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ import { readFile, rm, stat, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { promisify } from "node:util";
import { gzip } from "node:zlib";
import { processBinary } from "binpunch";
import { build as esbuild } from "esbuild";
import { uploadSourcemaps } from "../src/lib/api/sourcemaps.js";
import { injectDebugId, PLACEHOLDER_DEBUG_ID } from "./debug-id.js";
Expand Down Expand Up @@ -321,6 +320,7 @@ async function compileAllTargets(
[
fossilizeBin,
"--no-bundle",
"--hole-punch",
"--output-name",
"sentry",
"--platforms",
Expand All @@ -342,7 +342,7 @@ async function compileAllTargets(
return { successes: 0, failures: targets.length };
}

// Post-process each target: rename Windows binary, hole-punch ICU, gzip
// Post-process each target: rename Windows binary, gzip
let successes = 0;
let failures = 0;
for (const target of targets) {
Expand All @@ -361,10 +361,13 @@ async function compileAllTargets(

/**
* Post-process a single compiled binary: rename from fossilize's output
* naming to our expected naming, hole-punch ICU data, and optionally gzip.
* naming to our expected naming, and optionally gzip.
*
* Fossilize outputs `sentry-{os}-{arch}[.exe]` where os is "win" for Windows.
* We rename "win" → "windows" to match our release naming convention.
*
* Note: ICU hole-punch now runs inside fossilize (--hole-punch flag) before
* code signing, so it's no longer done here.
*/
async function postProcessTarget(target: BuildTarget): Promise<void> {
const packageName = getPackageName(target);
Expand All @@ -383,15 +386,6 @@ async function postProcessTarget(target: BuildTarget): Promise<void> {

console.log(` -> ${outfile}`);

// Hole-punch: zero unused ICU data entries so they compress to nearly nothing.
// Always runs so the smoke test exercises the same binary as the release.
const hpStats = processBinary(outfile);
if (hpStats && hpStats.removedEntries > 0) {
console.log(
` -> hole-punched ${hpStats.removedEntries}/${hpStats.totalEntries} ICU entries`
);
}

// On main and release branches (RELEASE_BUILD=1), create gzip-compressed
// copies for release downloads / GHCR nightly (~70% smaller with hole-punch).
if (process.env.RELEASE_BUILD) {
Expand Down
18 changes: 17 additions & 1 deletion src/commands/cli/upgrade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -432,7 +432,23 @@ async function spawnWithRetry(
env,
});
return await new Promise<number>((resolve, reject) => {
proc.on("close", (code) => resolve(code ?? 1));
proc.on("close", (code, signal) => {
// SIGKILL on macOS typically means AMFI killed the binary due to
// an invalid code signature (the downloaded binary has quarantine
// xattr). Surface this clearly instead of an opaque exit code.
if (signal === "SIGKILL") {
reject(
new UpgradeError(
"execution_failed",
"Downloaded binary was killed by the operating system (SIGKILL). " +
"This usually means the binary has an invalid code signature. " +
"Try reinstalling: curl -sL https://sentry.io/get-cli/ | bash"
)
);
return;
}
resolve(code ?? 1);
});
proc.on("error", (err) => reject(err));
});
} catch (error) {
Expand Down
Loading