#!/usr/bin/env bash # ============================================================================= # upload_sentry_symbols.sh # # Uploads Rust debug symbols and source maps to Sentry for the Tauri app. # This enables proper stack trace symbolication in Sentry for production builds. # # Usage: # ./scripts/upload_sentry_symbols.sh [version] # # Environment variables required: # SENTRY_AUTH_TOKEN - Sentry authentication token (required) # SENTRY_ORG - Sentry organization slug (required) # SENTRY_PROJECT - Sentry project name (required) # # Optional environment variables: # SENTRY_VERSION - Release version (defaults to: openhuman@{version}) # DEBUG_SYMBOLS_PATH - Path to debug symbols (defaults to: target/release/deps) # ============================================================================= set -euo pipefail # Color output helpers RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' NC='\033[0m' # No Color log_info() { echo -e "${GREEN}[INFO]${NC} $1" } log_warn() { echo -e "${YELLOW}[WARN]${NC} $1" } log_error() { echo -e "${RED}[ERROR]${NC} $1" } # Validate required environment variables check_env_vars() { local missing_vars=() if [[ -z "${SENTRY_AUTH_TOKEN:-}" ]]; then missing_vars+=("SENTRY_AUTH_TOKEN") fi if [[ -z "${SENTRY_ORG:-}" ]]; then missing_vars+=("SENTRY_ORG") fi if [[ -z "${SENTRY_PROJECT:-}" ]]; then missing_vars+=("SENTRY_PROJECT") fi if [[ ${#missing_vars[@]} -gt 0 ]]; then log_error "Missing required environment variables: ${missing_vars[*]}" log_error "Please set these variables before running this script." exit 1 fi } # Detect or install sentry-cli ensure_sentry_cli() { if command -v sentry-cli &> /dev/null; then log_info "sentry-cli already installed: $(sentry-cli --version)" return 0 fi log_info "Installing sentry-cli..." # Detect OS and architecture. The release-asset suffix matches what # `getsentry/sentry-cli` actually publishes — OS segment is # title-cased (Linux/Darwin/Windows), not lowercase. Lowercase 404s # silently and we end up writing GitHub's HTML error page to a file # the script then tries to execute ("Not: command not found"). local os_arch case "$(uname -s)" in Linux*) case "$(uname -m)" in x86_64|amd64) os_arch="Linux-x86_64" ;; aarch64|arm64) os_arch="Linux-aarch64" ;; *) log_error "Unsupported architecture: $(uname -m)" exit 1 ;; esac ;; Darwin*) # The mac build is published as a universal binary, not # per-arch, so both Intel and Apple Silicon use the same # asset — there is no Darwin-x86_64 / Darwin-arm64. os_arch="Darwin-universal" ;; MINGW*|CYGWIN*|MSYS*) os_arch="Windows-x86_64.exe" ;; *) log_error "Unsupported operating system: $(uname -s)" exit 1 ;; esac # `2.34.2` was never a real release of getsentry/sentry-cli (we presumably # confused it with python-sentry-sdk versions — sentry-python ships those # numbers, sentry-cli's 2.x series is gone from the releases page). Pin # to 3.4.1, the latest stable that matches the `--log-level=warn` flag # the upload step uses (2.x called it `--log-level=warning`). Override # via SENTRY_CLI_VERSION if a future bump is needed. local version="${SENTRY_CLI_VERSION:-3.4.1}" local download_url="https://github.com/getsentry/sentry-cli/releases/download/${version}/sentry-cli-${os_arch}" # Create temporary directory for installation. Cleanup is inline at the # success path + each early-exit branch below — we deliberately do NOT # use a trap. Bash traps are globally scoped (not function-scoped), so # `trap '... $tmp_dir ...' RETURN` defined here fires on EVERY # subsequent function return (including main's at end-of-script) by # which point `local tmp_dir` is gone and `set -u` errors with # "tmp_dir: unbound variable". An EXIT trap has the same problem. local tmp_dir tmp_dir="$(mktemp -d)" # Download and install. `--fail` / `--fail-with-body` is critical: # without it, curl returns 0 on a 404 and writes the error HTML to # the destination file. Same for wget without `--content-on-error`. log_info "Downloading sentry-cli ${version} for ${os_arch}..." if command -v curl &> /dev/null; then curl --fail --silent --show-error --location "${download_url}" -o "${tmp_dir}/sentry-cli" || { log_error "Failed to download sentry-cli from ${download_url}" rm -rf "$tmp_dir" exit 1 } elif command -v wget &> /dev/null; then wget --quiet --show-progress=off "${download_url}" -O "${tmp_dir}/sentry-cli" || { log_error "Failed to download sentry-cli from ${download_url}" rm -rf "$tmp_dir" exit 1 } else log_error "Neither curl nor wget found. Cannot download sentry-cli." rm -rf "$tmp_dir" exit 1 fi # Validate the downloaded file is actually an executable, not an HTML # error page that slipped through (defence-in-depth alongside --fail). if [[ ! -s "${tmp_dir}/sentry-cli" ]]; then log_error "sentry-cli download is empty" rm -rf "$tmp_dir" exit 1 fi if head -c 4 "${tmp_dir}/sentry-cli" | grep -q '^<'; then log_error "sentry-cli download looks like HTML (got an error page from ${download_url})" rm -rf "$tmp_dir" exit 1 fi # Make executable and install to ~/.cargo/bin or /usr/local/bin chmod +x "${tmp_dir}/sentry-cli" local install_dir="${HOME}/.cargo/bin" mkdir -p "${install_dir}" if [[ -w "${install_dir}" ]]; then mv "${tmp_dir}/sentry-cli" "${install_dir}/sentry-cli" log_info "sentry-cli installed to ${install_dir}/sentry-cli" else # Fallback to /usr/local/bin (may require sudo) if sudo mv "${tmp_dir}/sentry-cli" "/usr/local/bin/sentry-cli" 2>/dev/null; then log_info "sentry-cli installed to /usr/local/bin/sentry-cli" else log_error "Cannot write to ${install_dir} or /usr/local/bin. Please install sentry-cli manually." rm -rf "$tmp_dir" exit 1 fi fi # `mv` already emptied tmp_dir of the binary; rmdir the now-empty # directory so we don't leak it on every CI run. rm -rf "$tmp_dir" # Update PATH hash for current session (won't persist without shell restart) hash -r } # Upload debug symbols to Sentry upload_symbols() { local version="${1:-}" local symbols_path="${2:-target/release/deps}" if [[ -z "${version}" ]]; then log_error "Version is required" exit 1 fi # Honor SENTRY_RELEASE if set so DIFs attach to the same release name # the running binaries report (`openhuman@+`). Without this, # CI uploads to `openhuman@` while events are tagged # `openhuman@+` — a different release, so Sentry never # joins frames to symbols and stack traces stay un-symbolicated. # Falls back to the bare-version tag for local invocations that don't # set SENTRY_RELEASE. local release_name="${SENTRY_RELEASE:-openhuman@${version}}" log_info "Uploading Rust debug symbols for release: ${release_name}" log_info "Symbols path: ${symbols_path}" # Create Sentry release log_info "Creating/updating Sentry release..." sentry-cli releases new "${release_name}" || true # Use --ignore-missing for shallow clones or CI environments sentry-cli releases set-commits --auto --ignore-missing "${release_name}" || true # Upload debug symbols + source bundles. `--include-sources` makes # `sentry-cli` package the referenced source files into a `.src.zip` # alongside the DIF, so Sentry renders surrounding source lines in # Rust stack traces instead of bare `function + 0xNNN`. CI runs from a # full workspace checkout, so the source paths embedded in the DWARF # resolve and the bundle is built correctly. log_info "Uploading debug symbols..." local upload_args=( "upload-dif" "--org" "${SENTRY_ORG}" "--project" "${SENTRY_PROJECT}" "--include-sources" # sentry-cli 3.x renamed `warning` → `warn`. Use the short form; # `warning` is rejected as `invalid value '...' for '--log-level'` # on 3.x and the script silently skips uploads. "--log-level=warn" ) # Find and upload all debug symbol files. The output is captured so the # script can verify *something* was actually uploaded — sentry-cli exits # 0 even when it found zero DIFs, which silently breaks symbolication # for the Tauri shell and standalone core CLI exactly the way #1403 # caught for the frontend. Fail loudly here so CI catches it on the # build that produced the empty target dir, not weeks later when an # event arrives unsymbolicated. if [[ ! -d "${symbols_path}" ]]; then log_error "Symbols path does not exist: ${symbols_path}" log_error "Expected Cargo target dir with build artifacts. Did the build step complete?" exit 1 fi log_info "Scanning for debug symbols in ${symbols_path}..." local upload_log upload_log="$(mktemp)" if ! sentry-cli "${upload_args[@]}" "${symbols_path}" 2>&1 | tee "${upload_log}"; then log_error "sentry-cli upload-dif exited non-zero" rm -f "${upload_log}" exit 1 fi # sentry-cli prints "Found N debug information files" when scanning, and # "Uploaded N missing debug information files" / "No new debug # information files to upload" after the upload phase. We accept either # "Found > 0" or "Uploaded > 0" — the second covers re-runs where the # DIFs are already on Sentry's side. Empty input dirs print neither and # fall through to the failure branch. local found=0 uploaded=0 already=0 if grep -qE 'Found [1-9][0-9]* debug information' "${upload_log}"; then found=1 fi if grep -qE 'Uploaded [1-9][0-9]* missing debug information' "${upload_log}"; then uploaded=1 fi if grep -qE 'No new debug information files to upload|already exist' "${upload_log}"; then already=1 fi rm -f "${upload_log}" if [[ "${found}" -eq 0 && "${uploaded}" -eq 0 && "${already}" -eq 0 ]]; then log_error "sentry-cli upload-dif found zero debug information files in ${symbols_path}" log_error "Production Sentry events from this build will NOT be symbolicated. (#1403)" log_error "Likely causes: build profile produced no DWARF/PDB/dSYM, wrong target dir, or ${symbols_path} was cleaned before this step." exit 1 fi # Finalize the release log_info "Finalizing release..." sentry-cli releases finalize "${release_name}" log_info "Successfully uploaded symbols for ${release_name}" } # Main execution main() { log_info "=== Sentry Symbol Upload Script ===" # Parse arguments local version="${1:-}" local symbols_path="${2:-}" # Check environment variables check_env_vars # Ensure sentry-cli is available ensure_sentry_cli # Validate version argument if [[ -z "${version}" ]]; then # Try to extract version from Cargo.toml or package.json if [[ -f "app/src-tauri/Cargo.toml" ]]; then version=$(grep -m1 '^version\s*=' app/src-tauri/Cargo.toml | sed 's/version\s*=\s*"\([^"]*\)"/\1/') log_info "Detected version from Cargo.toml: ${version}" elif [[ -f "app/package.json" ]]; then version=$(grep -m1 '"version"' app/package.json | sed 's/.*"version": *"\([^"]*\)".*/\1/') log_info "Detected version from package.json: ${version}" else log_error "Could not determine version. Please provide it as an argument." log_info "Usage: $0 [symbols_path]" exit 1 fi fi # Default symbols path if not provided if [[ -z "${symbols_path}" ]]; then symbols_path="target/release/deps" fi # Upload symbols upload_symbols "${version}" "${symbols_path}" log_info "=== Upload complete ===" } # Run main function main "$@"