mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-28 05:12:33 +00:00
761 lines
39 KiB
YAML
761 lines
39 KiB
YAML
---
|
|
# 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 }}
|
|
# 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: 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@<version>+<sha> 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@<version>+<sha>`) 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/<version>/<os-arch>/` 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' || '' }}
|