Dashboard › cli › Distillation
f9880634-451b-4111-a610-05e729eeb10a["lore_tm_v1_2Y1jmlN8Z0dW9Up1gMkGzdnZzhcs41_k1ETLOWyTQHc","lore_tm_v1_UrzmYsPNHNMw9sJT9XSu0BF4SfKm8DVUXPGuVTiyUDU","lore_tm_v1_wAbtyewWukgXmxVjIfzLjS1A0uONBPm17Oy9maW7LqM","lore_tm_v1_3PkITIQ0MSznOOg9gV1Nr6NrHB9gpYSBt302ezJV3To","lore_tm_v1_78GD0EZLZfuKFOU-oU5kMBGYWx4IMRGGT3Uoox1gS2g"]
🔴 (20:40) .github/workflows/ci.yml job build-npm, named Build npm Package (smoke Node ${{ matrix.node }}), needs lint and test-unit, runs on ubuntu-latest, uses fail-fast: false, and tests matrix Node majors "20", "22", and "24".
🔴 (20:40) build-npm selects the production environment only on main or release/ refs, builds under ${{ env.NODE_VERSION_22 }} because script/require-shim.mjs uses node:module APIs available only on Node 22.15+, and the bundle is “always produced on a modern” Node to match publication.
🔴 (20:40) build-npm directive: “Switch to the matrix Node to run the produced artifact.” Runtime mapping is Node 24 → ${{ env.NODE_VERSION_24 }}, Node 20 → ${{ env.NODE_VERSION_20 }}, otherwise ${{ env.NODE_VERSION_22 }}; Node 20 exercises the bundled WASM SQLite fallback because node:sqlite is unavailable before 22.15, while Node 22/24 use the native driver.
🔴 (20:40) build-npm caches root/package/app node_modules under key node-modules-${{ hashFiles('pnpm-lock.yaml', '.npmrc', 'packages/cli/patches/**') }}, runs pnpm run bundle with environment-scoped SENTRY_AUTH_TOKEN, then clears SENTRY_AUTH_TOKEN and SENTRY_TOKEN for smoke tests and sets SENTRY_CONFIG_DIR=${{ runner.temp }}/.sentry-smoke.
🔴 (20:40) build-npm runs basic smoke command node packages/cli/dist/bin.cjs --help; its deep smoke runs node packages/cli/dist/bin.cjs auth status, requires exit code 10 (AUTH_NOT_AUTHENTICATED) and case-insensitive output containing not authenticated, and on Node 20 exercises SQLite initialization, schema migrations, telemetry lazy import, and the CJS require chain entirely through bundled WASM SQLite.
🔴 (20:40) build-npm runs npm pack in packages/cli; only matrix entry matrix.node == '22' uploads packages/cli/*.tgz through actions/upload-artifact@v7 as artifact npm-package.
🔴 (20:40) .github/workflows/ci.yml job build-docs needs lint and build-binary, runs on ubuntu-latest, selects production only on main or release/, and sets job-level SENTRY_AUTH_TOKEN so its if: env.SENTRY_AUTH_TOKEN != '' guard resolves after environment application.
🔴 (20:40) build-docs pins ${{ env.NODE_VERSION_24 }} because Astro 6 requires Node >= 22.12, reads packages/cli/package.json into output version, downloads artifact sentry-linux-x64 to dist-bin, runs chmod +x dist-bin/sentry-linux-x64, and generates content with pnpm run generate:schema && pnpm run generate:docs.
🔴 (20:40) build-docs builds apps/cli-docs with PUBLIC_SENTRY_ENVIRONMENT=production, SENTRY_RELEASE=${{ steps.version.outputs.version }}, and PUBLIC_SENTRY_RELEASE=${{ steps.version.outputs.version }}; it runs pnpm install --frozen-lockfile followed by pnpm run build.
🔴 (20:40) On pushes with a nonempty SENTRY_AUTH_TOKEN, build-docs injects debug IDs and uploads sourcemaps using ./dist-bin/sentry-linux-x64 sourcemap inject apps/cli-docs/dist/ and ./dist-bin/sentry-linux-x64 sourcemap upload apps/cli-docs/dist/ --release "${{ steps.version.outputs.version }}" --url-prefix "~/", with SENTRY_ORG=sentry and SENTRY_PROJECT=cli-website.
🔴 (20:40) After sourcemap upload, build-docs deletes apps/cli-docs/dist/**/*.map, copies .nojekyll into the output, packages the directory as $GITHUB_WORKSPACE/gh-pages.zip, and uploads it via actions/upload-artifact@v7 as artifact gh-pages.
🔴 (20:40) .github/workflows/ci.yml job ci-status uses if: always(), needs changes, check-generated, build-binary, build-npm, build-docs, test-e2e, generate-patches, and codemod-test, and fails on any explicit failure or cancelled result; publish-nightly is intentionally excluded because it is GHCR infrastructure rather than code quality.
🔴 (20:40) ci-status treats generate-patches being skipped on pull requests as expected, but detects cascaded upstream failures when code changes make test-e2e skipped, skill changes make check-generated skipped, or codemod changes make codemod-test skipped.
🔴 (20:40) packages/cli/src/lib/ghcr.ts uses GHCR_REQUEST_TIMEOUT=10_000, GHCR_MAX_RETRIES=1, and GHCR_BLOB_TIMEOUT=30_000; fetchWithRetry() retries timeout/abort, ECONNRESET, ECONNREFUSED, network, and fetch failed errors, but immediately propagates caller-triggered external aborts and does not retry HTTP-level errors.
🔴 (20:40) packages/cli/src/lib/ghcr.ts combines AbortSignal.timeout(timeout) with an optional caller signal via AbortSignal.any(), wraps exhausted requests in UpgradeError("network_error", ...), and identifies external cancellation only when the provided signal is aborted and the error name is AbortError.
🔴 (20:40) GHCR distribution constants are GHCR_REPO="getsentry/cli", GHCR_TAG="nightly", registry https://ghcr.io, OCI manifest media type application/vnd.oci.image.manifest.v1+json, tag page size TAGS_PAGE_SIZE=100, ordinary request timeout 10 seconds, and initial GHCR blob timeout 30 seconds.
🔴 (20:40) getAnonymousToken(signal?) requests https://ghcr.io/token?scope=repository:getsentry/cli:pull without credentials, requires a successful response containing token, and otherwise raises UpgradeError messages including GHCR token exchange failed: HTTP ${response.status} or GHCR token exchange returned no token.
🔴 (20:40) fetchManifest(token, tag, signal?) requests /v2/getsentry/cli/manifests/${tag} with bearer authorization, the OCI manifest Accept header, and the CLI user agent; getNightlyVersion() reads manifest.annotations?.version, while findLayerByFilename() matches org.opencontainers.image.title and throws UpgradeError("version_not_found", \No nightly build found for ${filename}`)` if absent.
🔴 (20:40) downloadNightlyBlob() manually requests the GHCR blob with redirect: "manual" because GHCR may return 301/302/307/308 to Azure Blob Storage; it follows the Location without the bearer Authorization header because forwarding that header alongside Azure’s signed query credentials yields HTTP 404.
🔴 (20:40) The redirected Azure download deliberately has no AbortSignal.timeout; its optional caller signal remains supported, while the initial GHCR request retains the 30-second timeout. Rationale: a full nightly binary is approximately 30 MB, and a 30-second body-stream timeout would require sustained throughput of roughly 8 Mbps and fail on slow connections.
🔴 (20:40) listTags(token, prefix?, signal?) paginates /v2/getsentry/cli/tags/list?n=100 using &last=${encodeURIComponent(lastTag)}, optionally retains tags beginning with prefix, stops on an empty or short page, and returns the accumulated registry-order tags; downloadLayerBlob() fully buffers downloadNightlyBlob() as an ArrayBuffer for small patch payloads around 50–500 KB.
🔴 (20:41) packages/cli/src/lib/upgrade.ts getCurlInstallPaths() prioritizes a stored curl install path only if existsSync(dirname(stored.path)), then checks process.execPath against known curl directories, then falls back to ~/.sentry/bin/${getBinaryFilename()}; this avoids stale SENTRY_INSTALL_DIR database rows causing ENOENT ... open '.../sentry.lock'.
🔴 (20:41) fetchLatestNightlyVersion(signal?) currently checks the optional signal before each network call but invokes getAnonymousToken() and fetchNightlyManifest(token) without passing it; it performs the anonymous token exchange plus rolling :nightly manifest fetch and returns the manifest’s version annotation.
🔴 (20:41) nightlyVersionExists(version) checks GHCR tag nightly-${version} and maps only UpgradeError messages containing HTTP 404 or HTTP 403 to false; other errors propagate. versionExists() routes nightly versions to GHCR, stable curl/brew versions to a GitHub Releases HEAD, and stable package-manager versions to an npm registry HEAD.
🔴 (20:41) streamDecompressToFile() in packages/cli/src/lib/upgrade.ts pipes through new DecompressionStream("gzip"), writes with createWriteStream(destPath), and reports indeterminate decompressed bytes using makeByteProgress("Downloading", null, setMessage) because compressed Content-Length does not reveal final size.
🔴 (20:41) streamDecompressToFile() captures writer errors such as ENOSPC and EIO; when backpressure occurs it races one-time drain and error listeners because an I/O failure while the buffer is full may “never emit 'drain',” which otherwise causes a hang, and it removes the unused listener to avoid MaxListenersExceededWarning.
🔴 (20:41) Writer lifecycle directive in streamDecompressToFile(): “Always flush/close the writer.” It calls writer.end(...) after streaming; if streaming already failed, the original stream error remains authoritative and a later writer.end() rejection is demoted to log.debug, preventing cleanup failure from masking the root cause.
🔴 (20:41) downloadNightlyToPath() gets an anonymous token, fetches either nightly-${version} or rolling :nightly, finds ${getPlatformBinaryName()}.gz, downloads its layer digest, requires a response body, and streams gzip decompression to the destination.
🔴 (20:41) downloadStableToPath() first tries ${url}.gz because the compressed asset is about 37 MB versus about 99 MB raw and roughly 60% smaller; any compressed-path failure falls back to the raw URL, whose response is fully consumed with response.arrayBuffer() before writeFile(destPath, new Uint8Array(body)) to avoid Bun issue oven-sh/bun#13237.
🔴 (20:41) Binary visibility verification uses VERIFY_MAX_ATTEMPTS=6 and VERIFY_BASE_DELAY_MS=100; waitForBinaryVisible() probes at 0, 100, 300, 700, 1500, and 3100 ms, accepting only a regular file with size greater than zero.
🔴 (20:41) waitForBinaryVisible() addresses Windows + Bun 1.3.9 issue CLI-1D3, where streaming writer completion may precede filesystem visibility and a subsequent spawn fails with Executable not found in $PATH; it throws UpgradeError("execution_failed", ...) when the file “never becomes visible or stays empty.”
🔴 (20:41) downloadBinaryToTemp() acquires and retains the curl-install lock across download→spawn→install, deletes a leftover temp file, attempts delta upgrade first, handles explicit and network-fallback offline cache misses with distinct messages, otherwise performs a full download, verifies the temp file with waitForBinaryVisible(), logs its exact formatted size, clears consumed patch cache best-effort, and applies mode 0o755 outside Windows.