--- # Reusable workflow that owns the desktop build + sign + Sentry-DIF + # artifact-upload matrix. Both `release-production.yml` and # `release-staging.yml` `uses:` this workflow so the build code lives in # exactly one place. Variation between the two flows (release vs debug # profile, mac notarization on/off, GH Release vs Actions-artifact # uploads, standalone-CLI sidecar build, env labels for telegram / # Sentry / API base URL) is driven by inputs below. # # `secrets: inherit` on the caller side gives this workflow access to # the repo's secrets without having to enumerate them; vars are read # directly from the `vars` context. name: Build Desktop (reusable) on: workflow_call: inputs: build_ref: description: Git ref to check out for the build (tag or SHA). type: string required: true tag: description: Tag name used by GH Release uploads (e.g. v1.2.4) and by the staging standalone CLI artifact name (e.g. v1.2.4-staging). type: string required: true version: description: Plain SemVer version (no v prefix), used in SENTRY_RELEASE. type: string required: true sha: description: Full commit SHA the build is pinned to. type: string required: true short_sha: description: 12-char prefix of `sha` matching the runtime truncation in config.ts / vite.config.ts / main.rs / app/src-tauri/src/lib.rs. type: string required: true base_url: description: Backend API base URL baked into the bundle. type: string required: true app_env: description: APP_ENVIRONMENT label baked into the bundle (production | staging). type: string required: true build_profile: description: Cargo profile to build (release | debug). type: string required: true telegram_bot_username: description: Telegram bot handle baked into the bundle. type: string required: true with_macos_signing: description: When true, run the sign + notarize + repackage-DMG path for the macOS matrix entries. Default true — both production and staging ship notarized macOS bundles so Gatekeeper accepts the staging build the same way it accepts production. Disable only for fast local-style dry runs that intentionally skip Apple's notary service. type: boolean default: true with_release_upload: description: When true, upload installer assets to the GitHub Release identified by `tag`. When false, upload bundles as Actions artifacts instead. type: boolean default: false release_id: description: Release ID used by the macOS re-upload script. Only consulted when `with_release_upload` and `with_macos_signing` are both true. type: string default: "" build_sidecar: description: When true, build the standalone openhuman-core CLI binary alongside the Tauri shell, stage it for the bundler, upload its DIFs to the core Sentry project, and publish it as an Actions artifact. Staging uses this; production builds do not (the core lives in-process in the Tauri shell since #1061). type: boolean default: false with_updater: description: When true, set `WITH_UPDATER=true` so the Tauri bundler emits the signed `.sig` updater artifacts the auto-updater consumes. Production sets this and assembles `latest.json` in a follow-on job; staging leaves it off because there is no manifest publish step to consume the .sig files — producing them just wastes signing time and pollutes the artifact tree. type: boolean default: true jobs: build: name: "Desktop: ${{ matrix.settings.artifact_suffix }}" runs-on: ${{ matrix.settings.platform }} timeout-minutes: 70 environment: Production strategy: fail-fast: false matrix: settings: - platform: macos-latest args: --target aarch64-apple-darwin target: aarch64-apple-darwin artifact_suffix: aarch64-apple-darwin - platform: macos-latest args: --target x86_64-apple-darwin target: x86_64-apple-darwin artifact_suffix: x86_64-apple-darwin - platform: ubuntu-24.04 args: --target x86_64-unknown-linux-gnu --bundles deb appimage target: x86_64-unknown-linux-gnu artifact_suffix: ubuntu - platform: ubuntu-24.04-arm args: --target aarch64-unknown-linux-gnu --bundles deb appimage target: aarch64-unknown-linux-gnu artifact_suffix: ubuntu-arm64 - platform: windows-latest args: --target x86_64-pc-windows-msvc target: x86_64-pc-windows-msvc artifact_suffix: windows env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Consumed by scripts/ci-cancel-aware.sh's cancellation watchdog. GH_TOKEN: ${{ github.token }} # Keep in sync with DEFAULT_TELEGRAM_BOT_USERNAME_* in channels/controllers/ops.rs OPENHUMAN_TELEGRAM_BOT_USERNAME: ${{ inputs.telegram_bot_username }} VITE_TELEGRAM_BOT_USERNAME: ${{ inputs.telegram_bot_username }} steps: - name: Checkout build ref uses: actions/checkout@v5 with: ref: ${{ inputs.build_ref }} fetch-depth: 1 submodules: recursive - name: Log build policy shell: bash run: | echo "[build-desktop] tag=${{ inputs.tag }} build_ref=${{ inputs.build_ref }}" - name: Set Xcode version if: matrix.settings.platform == 'macos-latest' uses: maxim-lobanov/setup-xcode@v1 with: xcode-version: latest-stable - name: Setup pnpm uses: pnpm/action-setup@v5 with: cache: true - name: Setup Node.js 24.x uses: actions/setup-node@v5 with: node-version: 24.x - name: Install Rust (rust-toolchain.toml) uses: dtolnay/rust-toolchain@1.93.0 with: targets: ${{ matrix.settings.platform == 'macos-latest' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }} - name: Ensure macOS cross target on the toolchain-file toolchain if: matrix.settings.platform == 'macos-latest' run: | # rust-toolchain.toml pins 1.96.1, which overrides the dtolnay-installed # 1.93.0. The action's `targets:` input added the darwin targets to # 1.93.0, not the override toolchain the vendored tauri-cli's CEF # universal-helper build actually uses, so add them to the active # (override) toolchain here to avoid E0463 "can't find crate for `core`". cargo --version rustup target add aarch64-apple-darwin x86_64-apple-darwin - name: Install Tauri dependencies (ubuntu only) if: startsWith(matrix.settings.platform, 'ubuntu-') run: | sudo apt-get update sudo apt-get install -y \ libgtk-3-dev libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev \ patchelf cmake libasound2-dev libxdo-dev libxtst-dev libx11-dev libxi-dev \ libevdev-dev libssl-dev libclang-dev desktop-file-utils \ libnss3 libnspr4 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm2 \ libxkbcommon0 libxcomposite1 libxdamage1 libxfixes3 libxrandr2 \ libgbm1 libpango-1.0-0 libcairo2 libatspi2.0-0 libxshmfence1 libu2f-udev # NOTE: The post-build dump lives further down (after `cargo tauri build`) # so the binary actually exists when ldd runs. Running it here pre-build # silently no-op'd and gave us no signal during the # quick-sharun "missing libraries" failures. # Skip first 7 lines of Cargo.lock (workspace package version bumps) so the key tracks dependency changes only - name: Cargo.lock fingerprint (deps only) id: cargo-lock-fingerprint shell: bash run: | echo "hash=$(tail -n +8 Cargo.lock | openssl dgst -sha256 | awk '{print $2}')" >> "$GITHUB_OUTPUT" - name: Cache Cargo registry and git sources uses: actions/cache@v5 with: path: | ~/.cargo/registry ~/.cargo/git key: ${{ runner.os }}-cargo-registry-${{ steps.cargo-lock-fingerprint.outputs.hash }} restore-keys: | ${{ runner.os }}-cargo-registry- # CEF is the runtime; cef-dll-sys + the vendored tauri-cli auto-download # the ~400MB Chromium distribution on first build. Cache per-OS across # runs. Default path is `dirs::cache_dir()/tauri-cef`. - name: Cache CEF binary distribution (unix) if: matrix.settings.platform != 'windows-latest' uses: actions/cache@v5 with: path: | ~/Library/Caches/tauri-cef ~/.cache/tauri-cef key: cef-${{ matrix.settings.target }}-${{ hashFiles('app/src-tauri/Cargo.toml') }} restore-keys: | cef-${{ matrix.settings.target }}- - name: Cache CEF binary distribution (windows) if: matrix.settings.platform == 'windows-latest' uses: actions/cache@v5 with: path: ~/AppData/Local/tauri-cef key: cef-${{ matrix.settings.target }}-${{ hashFiles('app/src-tauri/Cargo.toml') }} restore-keys: | cef-${{ matrix.settings.target }}- # Upstream @tauri-apps/cli doesn't bundle CEF framework files into the # produced installer. Only the fork at vendor/tauri-cef does. Build and # install that CLI so `cargo tauri build` invokes the cef-aware bundler. # # The cache key must include runner.arch, not just runner.os: cargo-tauri # is a host-arch binary, and the Linux matrix runs on both x86_64 # (ubuntu-24.04) and aarch64 (ubuntu-24.04-arm) runners that share # runner.os == 'Linux'. Keying on runner.os alone lets one arch restore # the other arch's binary, which then fails to exec ("ELF ...: not found" # / "Syntax error: word unexpected") when `cargo tauri build` runs it. - name: Cache vendored tauri-cli binary (unix) if: matrix.settings.platform != 'windows-latest' id: tauri-cli-cache-unix uses: actions/cache@v5 with: path: ~/.cargo/bin/cargo-tauri key: vendored-tauri-cli-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('app/src-tauri/vendor/tauri-cef/crates/tauri-cli/Cargo.toml') }} - name: Cache vendored tauri-cli binary (windows) if: matrix.settings.platform == 'windows-latest' id: tauri-cli-cache-windows uses: actions/cache@v5 with: path: ~/.cargo/bin/cargo-tauri.exe key: vendored-tauri-cli-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('app/src-tauri/vendor/tauri-cef/crates/tauri-cli/Cargo.toml') }} - name: Install vendored tauri-cli (cef-aware bundler) if: (matrix.settings.platform == 'windows-latest' && steps.tauri-cli-cache-windows.outputs.cache-hit != 'true') || (matrix.settings.platform != 'windows-latest' && steps.tauri-cli-cache-unix.outputs.cache-hit != 'true') shell: bash run: bash scripts/ci-cancel-aware.sh cargo install --locked --path app/src-tauri/vendor/tauri-cef/crates/tauri-cli - name: Install dependencies run: bash scripts/ci-cancel-aware.sh pnpm install --frozen-lockfile - name: Validate signing prerequisites # The minisign pubkey is baked into the static tauri.conf.json, not # injected from secrets; only the private key still has to come from # secrets, and only when `with_updater` is true (the bundler signs # `.sig` artifacts then). macOS additionally needs the Apple # notarization secret set when `with_macos_signing` is true. shell: bash env: MATRIX_PLATFORM: ${{ matrix.settings.platform }} WITH_MACOS_SIGNING: ${{ inputs.with_macos_signing }} WITH_UPDATER: ${{ inputs.with_updater }} TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY || secrets.UPDATER_PRIVATE_KEY }} APPLE_CERTIFICATE_BASE64: ${{ secrets.APPLE_CERTIFICATE_BASE64 }} APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} APPLE_ID: ${{ secrets.APPLE_ID }} # Match the secret name resolution used by the actual sign + notarize # steps below: prefer APPLE_APP_SPECIFIC_PASSWORD, fall back to the # legacy APPLE_PASSWORD. Validating a different secret than we use # would let runs pass the prereq check and then fail mid-notarization. APPLE_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD || secrets.APPLE_PASSWORD }} APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} run: | if [ "$WITH_UPDATER" = "true" ] && [ -z "$TAURI_SIGNING_PRIVATE_KEY" ]; then echo "Missing TAURI_SIGNING_PRIVATE_KEY (or fallback UPDATER_PRIVATE_KEY) and with_updater=true." exit 1 fi if [ "$MATRIX_PLATFORM" = "macos-latest" ] && [ "$WITH_MACOS_SIGNING" = "true" ]; then for var in APPLE_CERTIFICATE_BASE64 APPLE_CERTIFICATE_PASSWORD APPLE_SIGNING_IDENTITY APPLE_ID APPLE_PASSWORD APPLE_TEAM_ID; do if [ -z "${!var}" ]; then echo "Missing required macOS signing secret: $var" exit 1 fi done fi - name: Define Tauri configuration overrides # `prepareTauriConfig.js` only reads `WITH_UPDATER` (flips # `bundle.createUpdaterArtifacts` on for the release pipeline) and # `KEYPAIR_ALIAS` (Windows DigiCert sign command). Pubkey + endpoint # come from the static `app/src-tauri/tauri.conf.json`. id: config-overrides uses: actions/github-script@v8 env: BASE_URL: ${{ inputs.base_url }} WITH_UPDATER: ${{ inputs.with_updater && 'true' || 'false' }} with: script: | const workspacePath = process.env.GITHUB_WORKSPACE.replace(/\\/g, '/'); const prefix = workspacePath.startsWith('/') ? 'file://' : 'file:///'; const moduleUrl = `${prefix}${workspacePath}/scripts/prepareTauriConfig.js`; const { default: prepareTauriConfig } = await import(moduleUrl); const config = prepareTauriConfig(); core.setOutput('json', JSON.stringify(config)); # ---- Optional: standalone openhuman-core CLI sidecar ------------------ # Only built for staging (`build_sidecar: true`). Production builds the # core in-process inside the Tauri shell since #1061. - name: Resolve core manifest and binary names if: inputs.build_sidecar id: core-paths shell: bash env: MATRIX_TARGET: ${{ matrix.settings.target }} PROFILE: ${{ inputs.build_profile }} run: | if [ -f "openhuman_core/Cargo.toml" ]; then CORE_DIR="openhuman_core" elif [ -f "rust-core/Cargo.toml" ]; then CORE_DIR="rust-core" elif [ -f "Cargo.toml" ] && grep -q '^name = "openhuman"' Cargo.toml; then CORE_DIR="." else echo "No core Cargo manifest found (expected root Cargo.toml with openhuman, openhuman_core/Cargo.toml, or rust-core/Cargo.toml)" exit 1 fi SIDE_CAR_BASE="$(node -e "const fs=require('fs');const c=JSON.parse(fs.readFileSync('app/src-tauri/tauri.conf.json','utf8'));const b=(c.bundle&&Array.isArray(c.bundle.externalBin)&&c.bundle.externalBin[0])||'binaries/openhuman-core';process.stdout.write(String(b).split('/').pop());")" CORE_BIN_NAME="${SIDE_CAR_BASE}" echo "core_dir=$CORE_DIR" >> "$GITHUB_OUTPUT" echo "core_manifest=$CORE_DIR/Cargo.toml" >> "$GITHUB_OUTPUT" echo "core_target_dir=target/$MATRIX_TARGET/$PROFILE" >> "$GITHUB_OUTPUT" echo "core_bin_name=$CORE_BIN_NAME" >> "$GITHUB_OUTPUT" echo "sidecar_base=$SIDE_CAR_BASE" >> "$GITHUB_OUTPUT" - name: Build sidecar core binary if: inputs.build_sidecar shell: bash env: MATRIX_TARGET: ${{ matrix.settings.target }} CORE_MANIFEST: ${{ steps.core-paths.outputs.core_manifest }} CORE_BIN_NAME: ${{ steps.core-paths.outputs.core_bin_name }} OPENHUMAN_APP_ENV: ${{ inputs.app_env }} # Bake the short SHA into the CLI so build_release_tag() in # src/main.rs produces openhuman@+ matching the # Sentry release tag used when uploading the standalone CLI symbols. OPENHUMAN_BUILD_SHA: ${{ inputs.short_sha }} # Bake the core Sentry DSN into the binary via `option_env!` in # src/main.rs. Without this the standalone CLI ships with Sentry # disabled and the symbols uploaded below have nothing to attach # to. Same DSN as `release-packages.yml` uses for the Linux arm64 # tarball — there is one openhuman-core Sentry project that all # standalone-CLI surfaces report to. Prefer the namespaced GH # var, fall back to the legacy unprefixed one during transition. OPENHUMAN_CORE_SENTRY_DSN: ${{ vars.OPENHUMAN_CORE_SENTRY_DSN || vars.OPENHUMAN_SENTRY_DSN }} run: | if [ -z "${OPENHUMAN_CORE_SENTRY_DSN}" ]; then echo "::warning::vars.OPENHUMAN_CORE_SENTRY_DSN (or legacy vars.OPENHUMAN_SENTRY_DSN) is empty — the standalone CLI artifact will ship without crash reporting." fi bash scripts/ci-cancel-aware.sh cargo build \ --manifest-path "$CORE_MANIFEST" \ --target "$MATRIX_TARGET" \ --bin "$CORE_BIN_NAME" - name: Stage sidecar for Tauri bundler if: inputs.build_sidecar shell: bash run: | bash scripts/release/stage-sidecar.sh \ "${{ matrix.settings.target }}" \ "${{ steps.core-paths.outputs.core_target_dir }}" \ "${{ steps.core-paths.outputs.core_bin_name }}" \ "${{ steps.core-paths.outputs.sidecar_base }}" # ---- Tauri build ------------------------------------------------------- # Vite is invoked via tauri.conf.json's beforeBuildCommand inside # `cargo tauri build`, so all VITE_* env must be present here for the # bundle to bake them in. macOS signing is intentionally skipped here # and handled by the dedicated re-sign + notarize step further down # (hardened runtime + entitlements are required for notarization). - name: Build and package Tauri app (CEF, vendored CLI) id: tauri-build shell: bash working-directory: app env: BASE_URL: ${{ inputs.base_url }} OPENHUMAN_APP_ENV: ${{ inputs.app_env }} VITE_OPENHUMAN_APP_ENV: ${{ inputs.app_env }} VITE_BACKEND_URL: ${{ inputs.base_url }} VITE_GA_MEASUREMENT_ID: ${{ vars.VITE_GA_MEASUREMENT_ID }} # React frontend Sentry DSN — separate Sentry project. Baked by the # Vite plugin via `import.meta.env.VITE_SENTRY_DSN`. VITE_SENTRY_DSN: ${{ vars.OPENHUMAN_REACT_SENTRY_DSN }} # Tauri shell (desktop host) Sentry DSN. Baked into the shell binary # via `option_env!("OPENHUMAN_TAURI_SENTRY_DSN")` at compile time. OPENHUMAN_TAURI_SENTRY_DSN: ${{ vars.OPENHUMAN_TAURI_SENTRY_DSN }} # Bake the build SHA into the Tauri shell so its Sentry release tag # (`openhuman@+`) matches the React bundle and the # standalone CLI — events across all surfaces group under one release. OPENHUMAN_BUILD_SHA: ${{ inputs.sha }} VITE_DEBUG: ${{ vars.VITE_DEBUG }} VITE_BUILD_SHA: ${{ inputs.sha }} # Use short_sha (12 chars) — matches what config.ts / vite.config.ts # / main.rs / app/src-tauri/src/lib.rs all slice VITE_BUILD_SHA / # OPENHUMAN_BUILD_SHA down to at runtime when emitting events. The # sentry-vite-plugin reads SENTRY_RELEASE raw, so a long-SHA value # here would tag uploads against a different release than events. SENTRY_RELEASE: openhuman@${{ inputs.version }}+${{ inputs.short_sha }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_URL: ${{ vars.SENTRY_URL }} SENTRY_ORG: ${{ vars.SENTRY_ORG }} # SENTRY_PROJECT here is consumed by sentry-vite-plugin during the # Vite build that runs inside `cargo tauri build` — it uploads # frontend source maps to the React Sentry project. Both staging # and production push source maps (the plugin gates on # `SENTRY_AUTH_TOKEN`, which both callers inherit via # `secrets: inherit`); the React project differentiates the two # via the `environment` tag set at runtime in analytics.ts. Rust # DIFs go to the Tauri / core projects in the dedicated steps # below. SENTRY_PROJECT: ${{ vars.SENTRY_PROJECT_REACT }} MACOSX_DEPLOYMENT_TARGET: ${{ matrix.settings.platform == 'macos-latest' && '10.15' || '' }} TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD || secrets.UPDATER_PRIVATE_KEY_PASSWORD }} TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY || secrets.UPDATER_PRIVATE_KEY }} WITH_UPDATER: ${{ inputs.with_updater && 'true' || 'false' }} VITE_MINIMUM_SUPPORTED_APP_VERSION: ${{ vars.VITE_MINIMUM_SUPPORTED_APP_VERSION }} VITE_LATEST_APP_DOWNLOAD_URL: ${{ vars.VITE_LATEST_APP_DOWNLOAD_URL }} TAURI_CONFIG_OVERRIDE: ${{ steps.config-overrides.outputs.json }} MATRIX_ARGS: ${{ matrix.settings.args }} PROFILE_FLAG: ${{ inputs.build_profile == 'debug' && '--debug' || '' }} run: | # Inline NODE_OPTIONS so it reaches the vite child spawned by # beforeBuildCommand. Step-level env was observed not to propagate # on macos-arm64 runners, causing OOM at node's ~2GB auto default. # # Linux AppImage bundler note: the vendored tauri-cef bundler # invokes `quick-sharun.sh`, which runs `lib4bin` → `ldd` on the # staged OpenHuman binary. CEF (libcef.so) is downloaded into # `~/.cache/tauri-cef///` at cargo-build time and # copied into the AppDir at usr/lib/libcef.so, but the binary's # RUNPATH does not include either location, so plain `ldd` reports # `libcef.so => not found` and lib4bin aborts with the generic # "is missing libraries! Aborting..." banner. libcef.so is downloaded # by the cef-dll-sys build script into the CEF cache dir, which the # "Cache CEF binary distribution" step restores across runs. # # Build-once (#3877): this used to compile the whole app twice — a # throwaway `cargo tauri build --no-bundle` purely to fetch CEF, then # the real bundling build — which also re-ran the Vite/bundler # orchestration twice. We now resolve libcef.so cheaply and bundle in # a SINGLE `cargo tauri build`: # 1. Warm CEF cache (the common case): libcef.so is already restored, # so we skip straight to the single bundling build. # 2. Cold cache: prewarm with a TARGETED `cargo build -p cef-dll-sys` # (compiles only the CEF downloader crate + its deps, not the # whole app) to populate the cache. # 3. If that still didn't yield libcef.so, fall back to the # historical full `--no-bundle` prebuild — so correctness can # never regress relative to the old double build. # macOS / Windows take the single-call path unchanged. find_cef_lib_dir() { # `-quit` stops find after the first match so the `| head -1` can't # close the pipe early and SIGPIPE find. Under this step's # `set -o pipefail` + `-e`, that SIGPIPE (exit 141) aborted the whole # build before `cargo tauri build` ran whenever the warm cache held # more than one libcef.so (multiple version/arch dirs). Trailing # `|| true` also absorbs find's exit 1 when the cache dir is absent. find "$HOME/.cache/tauri-cef" -name libcef.so -printf '%h\n' -quit 2>/dev/null | head -1 || true } if [ "${RUNNER_OS}" = "Linux" ]; then CEF_LIB_DIR="$(find_cef_lib_dir)" if [ -z "$CEF_LIB_DIR" ]; then echo "[appimage-fix] CEF not cached — targeted prewarm via cef-dll-sys build script" # `cargo tauri build --debug` maps to cargo's default (dev) # profile; release maps to `--release`. Match the profile so the # prewarm's artifacts are reused by the bundling build below. CARGO_BUILD_PROFILE="--release" [ "$PROFILE_FLAG" = "--debug" ] && CARGO_BUILD_PROFILE="" NODE_OPTIONS="--max-old-space-size=8192" bash ../scripts/ci-cancel-aware.sh cargo build -p cef-dll-sys $CARGO_BUILD_PROFILE || true CEF_LIB_DIR="$(find_cef_lib_dir)" fi if [ -z "$CEF_LIB_DIR" ]; then echo "[appimage-fix] targeted prewarm did not yield libcef.so — falling back to full --no-bundle prebuild" NODE_OPTIONS="--max-old-space-size=8192" bash ../scripts/ci-cancel-aware.sh cargo tauri build --no-bundle $PROFILE_FLAG -c "$TAURI_CONFIG_OVERRIDE" $MATRIX_ARGS CEF_LIB_DIR="$(find_cef_lib_dir)" fi if [ -z "$CEF_LIB_DIR" ]; then echo "::error::libcef.so not found under ~/.cache/tauri-cef after prewarm; cannot satisfy lib4bin ldd resolution." >&2 exit 1 fi echo "[appimage-fix] prepending CEF lib dir to LD_LIBRARY_PATH: $CEF_LIB_DIR" export LD_LIBRARY_PATH="$CEF_LIB_DIR${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" fi NODE_OPTIONS="--max-old-space-size=8192" bash ../scripts/ci-cancel-aware.sh cargo tauri build $PROFILE_FLAG -c "$TAURI_CONFIG_OVERRIDE" $MATRIX_ARGS # Diagnostic for the recurring quick-sharun "is missing libraries! # Aborting..." error on the AppImage bundler — the upstream script # only prints a generic banner and swallows lib4bin's actual output, # so we have no visibility into which .so is unresolved. `if: always()` # ensures this still runs when `cargo tauri build` failed at bundling # (the binary itself is produced before bundling starts). - name: Dump linked libraries of built binary (ubuntu debug) if: always() && startsWith(matrix.settings.platform, 'ubuntu-') shell: bash env: PROFILE: ${{ inputs.build_profile }} MATRIX_TARGET: ${{ matrix.settings.target }} run: | set +e for BIN in \ "app/src-tauri/target/${MATRIX_TARGET}/${PROFILE}/OpenHuman" \ "app/src-tauri/target/${MATRIX_TARGET}/${PROFILE}/bundle/appimage_deb/data/usr/bin/OpenHuman" \ "target/${MATRIX_TARGET}/${PROFILE}/OpenHuman"; do if [ -x "$BIN" ]; then echo "==> ldd $BIN" ldd "$BIN" || true echo "==> unresolved:" ldd "$BIN" | grep 'not found' || echo " (all resolved)" echo fi done # Strip host graphics libraries (Mesa/libdrm/libva/libgbm) that lib4bin # ldd-walks into the AppImage. Those drivers must come from the user's # host system — even on a current Ubuntu runner the bundled Mesa can # fail to initialize newer GPUs (RDNA3, Intel Arc, Lovelace) and # crashes CEF on launch because the bundled libs shadow the host's # working ones on LD_LIBRARY_PATH. CEF's own SwiftShader subdir is # untouched. Re-signs the AppImage + updater tarball when signing # is enabled. - name: Strip host graphics libs from AppImage if: startsWith(matrix.settings.platform, 'ubuntu-') shell: bash env: MATRIX_TARGET: ${{ matrix.settings.target }} PROFILE: ${{ inputs.build_profile }} TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY || secrets.UPDATER_PRIVATE_KEY }} TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD || secrets.UPDATER_PRIVATE_KEY_PASSWORD }} run: | bash scripts/release/strip-appimage-graphics-libs.sh \ "app/src-tauri/target/${MATRIX_TARGET}/${PROFILE}/bundle" \ "target/${MATRIX_TARGET}/${PROFILE}/bundle" # Regression guard for #1403: if @sentry/vite-plugin silently no-op'd # (e.g. SENTRY_AUTH_TOKEN missing, or the `sourcemaps.assets` glob # didn't match dist/assets) production events arrive in Sentry as # unsymbolicated minified frames. The plugin logs a warning then # exits 0, so the only safe check is to inspect the shipped bundle # for injected debug-IDs. Skipped when SENTRY_AUTH_TOKEN is empty # (e.g. fork/PR builds where the upload was deliberately disabled). - name: Verify Sentry source-map upload (frontend) if: env.SENTRY_AUTH_TOKEN != '' shell: bash env: SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_URL: ${{ vars.SENTRY_URL }} run: node scripts/release/verify-sentry-sourcemaps.mjs # ---- Sentry DIF uploads ------------------------------------------------ # Since #1061 the core lives in-process as a library linked into the # Tauri shell binary — there is exactly one Rust process and one # `sentry::init` call (in `app/src-tauri/src/lib.rs::run()`), so all # Rust events from the desktop app route to `openhuman-tauri`. Upload # DIFs to that project so they actually attach to the right events. # Symbols are keyed by debug-ID, so it's safe to run per-matrix-target # without collisions — Sentry merges artifacts across platforms. - name: Upload Tauri shell debug symbols to Sentry (tauri project) if: env.SENTRY_AUTH_TOKEN != '' shell: bash env: SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_URL: ${{ vars.SENTRY_URL }} SENTRY_ORG: ${{ vars.SENTRY_ORG }} SENTRY_PROJECT: ${{ vars.SENTRY_PROJECT_TAURI }} SENTRY_RELEASE: openhuman@${{ inputs.version }}+${{ inputs.short_sha }} VERSION: ${{ inputs.version }} MATRIX_TARGET: ${{ matrix.settings.target }} PROFILE: ${{ inputs.build_profile }} run: | set -euo pipefail dif_dir="app/src-tauri/target/${MATRIX_TARGET}/${PROFILE}" # Hard-fail when the target dir is missing instead of silently # skipping (#1403). If we got past `cargo tauri build` with # SENTRY_AUTH_TOKEN set and this directory doesn't exist, the # build is broken and shipping it would leak un-symbolicated # crashes to production. if [ ! -d "$dif_dir" ]; then echo "::error::Tauri DIF dir not present: $dif_dir — cargo tauri build did not produce a target tree for ${MATRIX_TARGET}." >&2 exit 1 fi echo "==> Uploading symbols from $dif_dir to ${SENTRY_PROJECT}" bash scripts/upload_sentry_symbols.sh "$VERSION" "$dif_dir" # The standalone openhuman-core CLI has its own `sentry::init` in # `src/main.rs` and reports to `openhuman-core`. Only built when the # caller opts into `build_sidecar`; production currently does not. - name: Upload standalone core CLI debug symbols to Sentry (core project) if: inputs.build_sidecar && env.SENTRY_AUTH_TOKEN != '' shell: bash env: SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_URL: ${{ vars.SENTRY_URL }} SENTRY_ORG: ${{ vars.SENTRY_ORG }} SENTRY_PROJECT: ${{ vars.SENTRY_PROJECT_CORE }} SENTRY_RELEASE: openhuman@${{ inputs.version }}+${{ inputs.short_sha }} VERSION: ${{ inputs.version }} MATRIX_TARGET: ${{ matrix.settings.target }} PROFILE: ${{ inputs.build_profile }} run: | set -euo pipefail dif_dir="target/${MATRIX_TARGET}/${PROFILE}" # build_sidecar only runs `cargo build --bin openhuman` for the # standalone CLI, so this dir must exist when we reach this step. # Hard-fail on miss (#1403) so we never ship a sidecar with # un-symbolicated production crashes. if [ ! -d "$dif_dir" ]; then echo "::error::Core CLI DIF dir not present: $dif_dir — sidecar cargo build did not produce a target tree for ${MATRIX_TARGET}." >&2 exit 1 fi echo "==> Uploading symbols from $dif_dir to ${SENTRY_PROJECT}" bash scripts/upload_sentry_symbols.sh "$VERSION" "$dif_dir" # ---- Linux + Windows installer upload --------------------------------- # When uploading to a GH Release: push .deb / .AppImage / .msi / .exe # and their .sig siblings to the release. macOS goes through the # re-sign + notarize path below and uploads separately. - name: Upload non-macOS installers to release if: inputs.with_release_upload && matrix.settings.platform != 'macos-latest' shell: bash env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} TAG: ${{ inputs.tag }} MATRIX_TARGET: ${{ matrix.settings.target }} PROFILE: ${{ inputs.build_profile }} run: | set -euo pipefail shopt -s nullglob BUNDLE_ROOTS=( "app/src-tauri/target/${MATRIX_TARGET}/${PROFILE}/bundle" "target/${MATRIX_TARGET}/${PROFILE}/bundle" ) UPLOAD=() for root in "${BUNDLE_ROOTS[@]}"; do [ -d "$root" ] || continue for f in \ "$root"/deb/*.deb \ "$root"/appimage/*.AppImage \ "$root"/appimage/*.AppImage.sig \ "$root"/appimage/*.AppImage.tar.gz \ "$root"/appimage/*.AppImage.tar.gz.sig \ "$root"/msi/*.msi \ "$root"/msi/*.msi.sig \ "$root"/nsis/*-setup.exe \ "$root"/nsis/*-setup.exe.sig; do [ -e "$f" ] && UPLOAD+=("$f") done done if [ ${#UPLOAD[@]} -eq 0 ]; then echo "No installer artifacts found for ${MATRIX_TARGET}" exit 1 fi echo "Uploading:" printf ' %s\n' "${UPLOAD[@]}" gh release upload "$TAG" "${UPLOAD[@]}" --repo tinyhumansai/openhuman --clobber # ---- macOS sign / notarize / repackage -------------------------------- - name: Locate macOS .app bundle if: inputs.with_macos_signing && matrix.settings.platform == 'macos-latest' id: locate-app shell: bash env: MATRIX_TARGET: ${{ matrix.settings.target }} PROFILE: ${{ inputs.build_profile }} run: | set -euo pipefail APP_PATH="" for candidate in \ "app/src-tauri/target/${MATRIX_TARGET}/${PROFILE}/bundle/macos/OpenHuman.app" \ "target/${MATRIX_TARGET}/${PROFILE}/bundle/macos/OpenHuman.app"; do if [ -d "$candidate" ]; then APP_PATH="$candidate" break fi done if [ -z "$APP_PATH" ]; then APP_PATH="$(find . -path "*/${PROFILE}/bundle/macos/OpenHuman.app" -type d 2>/dev/null | head -1)" fi if [ -z "$APP_PATH" ]; then echo "ERROR: Could not find OpenHuman.app bundle anywhere" find . -name 'OpenHuman.app' -type d 2>/dev/null || true exit 1 fi BUNDLE_DIR="$(dirname "$(dirname "$APP_PATH")")" echo "app_path=$APP_PATH" >> "$GITHUB_OUTPUT" echo "bundle_dir=$BUNDLE_DIR" >> "$GITHUB_OUTPUT" echo "Found .app at: $APP_PATH" echo "Bundle dir: $BUNDLE_DIR" - name: Sign and notarize macOS .app if: inputs.with_macos_signing && matrix.settings.platform == 'macos-latest' shell: bash env: APPLE_CERTIFICATE_BASE64: ${{ secrets.APPLE_CERTIFICATE_BASE64 }} APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} APPLE_ID: ${{ secrets.APPLE_ID }} APPLE_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD || secrets.APPLE_PASSWORD }} APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} run: | bash scripts/release/sign-and-notarize-macos.sh \ "${{ steps.locate-app.outputs.app_path }}" \ "app/src-tauri/entitlements.sidecar.plist" - name: Re-package DMG after notarization if: inputs.with_macos_signing && matrix.settings.platform == 'macos-latest' shell: bash env: APPLE_ID: ${{ secrets.APPLE_ID }} APPLE_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD || secrets.APPLE_PASSWORD }} APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} run: | bash scripts/release/repackage-dmg.sh \ "${{ steps.locate-app.outputs.app_path }}" \ "${{ steps.locate-app.outputs.bundle_dir }}" - name: Re-upload notarized macOS artifacts to release if: inputs.with_macos_signing && inputs.with_release_upload && matrix.settings.platform == 'macos-latest' shell: bash env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} RELEASE_ID: ${{ inputs.release_id }} TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY || secrets.UPDATER_PRIVATE_KEY }} TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD || secrets.UPDATER_PRIVATE_KEY_PASSWORD }} run: | bash scripts/release/upload-macos-artifacts.sh \ "${{ steps.locate-app.outputs.app_path }}" \ "${{ steps.locate-app.outputs.bundle_dir }}" \ "${{ inputs.version }}" \ "${{ matrix.settings.target }}" - name: Verify macOS notarization staple if: inputs.with_macos_signing && matrix.settings.platform == 'macos-latest' shell: bash run: | APP_PATH="${{ steps.locate-app.outputs.app_path }}" echo "Checking staple at: $APP_PATH" xcrun stapler validate "$APP_PATH" || echo "WARNING: Staple validation failed" # ---- Actions-artifact uploads (when not pushing to a GH Release) ------ - name: Upload desktop bundles as Actions artifact if: "!inputs.with_release_upload" uses: actions/upload-artifact@v5 with: name: desktop-bundles-${{ matrix.settings.platform }}-${{ matrix.settings.artifact_suffix }} path: | app/src-tauri/target/${{ matrix.settings.target }}/${{ inputs.build_profile }}/bundle/** target/${{ matrix.settings.target }}/${{ inputs.build_profile }}/bundle/** - name: Upload standalone CLI artifact if: inputs.build_sidecar && !inputs.with_release_upload uses: actions/upload-artifact@v5 with: name: standalone-bins-${{ matrix.settings.platform }}-${{ matrix.settings.artifact_suffix }} path: | ${{ steps.core-paths.outputs.core_target_dir }}/${{ steps.core-paths.outputs.core_bin_name }}${{ matrix.settings.platform == 'windows-latest' && '.exe' || '' }}