mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-28 13:32:23 +00:00
* feat(voice): add dedicated voice assistance module for STT/TTS Extracts speech-to-text (whisper.cpp) and text-to-speech (piper) into a dedicated `src/openhuman/voice/` domain module with its own RPC namespace (`openhuman.voice_*`). Adds proactive availability checking via `voice_status` so the UI can show clear errors when binaries/models are missing instead of failing silently at transcription time. - New module: voice/types.rs, voice/ops.rs, voice/schemas.rs, voice/mod.rs - 4 RPC endpoints: voice_status, voice_transcribe, voice_transcribe_bytes, voice_tts - 21 unit tests + 1 integration test (json_rpc_e2e) - Frontend updated to use voice_* endpoints with status check on mode switch Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * style: fix cargo fmt in voice/ops.rs Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * test(e2e): add voice mode integration spec Tests switching to voice input mode, verifying status check fires, recording button renders, and switching back to text mode restores text input. Also checks reply mode toggle visibility. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: remove unused waitForText import in voice-mode e2e spec Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(voice): in-process whisper engine and LLM post-processing - Add whisper-rs (0.16) for in-process whisper.cpp inference, eliminating cold-start latency from subprocess-per-call (~1-3s) to warm inference (~50ms). Model is loaded once during bootstrap and reused across calls. Falls back to whisper-cli subprocess if in-process loading fails. - Add LLM post-processing layer that passes raw transcription through Ollama to fix grammar, punctuation, and filler words. Accepts optional conversation context to disambiguate names and technical terms. Gracefully degrades to raw whisper output if Ollama is unavailable. - Update voice RPC endpoints with new optional params (context, skip_cleanup) and return both cleaned text and raw_text. - Update frontend to pass conversation history as context for voice transcription cleanup, and update TypeScript interfaces to match. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * style: apply cargo fmt formatting fixes Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(build): make whisper-rs optional behind `whisper` feature flag The whisper-rs crate requires cmake to compile whisper.cpp from source, which is not available in the CI environment. Move it behind an optional cargo feature so CI builds succeed without cmake. The whisper_engine module now compiles as a no-op stub when the feature is disabled, returning "whisper feature not compiled in" errors. Desktop builds can opt in with `--features whisper`. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * style: cargo fmt whisper_engine.rs Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(ci): make whisper-rs mandatory and install cmake in CI Revert whisper-rs from optional to mandatory dependency. Add cmake installation to all CI workflows (build, typecheck, test, release) and the CI Docker image so whisper-rs can compile whisper.cpp from source. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * style: cargo fmt whisper_engine.rs Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address code review findings across voice module - whisper_engine: validate WAV sample rate (must be 16kHz) and channel count (1 or 2) before feeding audio to whisper - speech: offload load_engine and transcribe_in_process to tokio::task::spawn_blocking to avoid blocking the Tokio runtime - ops: use RAII guard for WHISPER_BIN env var in test to prevent races and ensure restore on panic; log temp file cleanup failures instead of silently ignoring; sanitize paths in debug logs to basenames only - postprocess: add test for disabled cleanup config returning raw text - voice-mode.spec: assert failure when neither voice CTA nor unavailable message appears; make reply mode test runnable in isolation with auth/nav setup Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * style: apply cargo fmt Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
734 lines
29 KiB
YAML
734 lines
29 KiB
YAML
name: Release
|
|
|
|
on:
|
|
workflow_dispatch:
|
|
inputs:
|
|
release_type:
|
|
description: Version increment type
|
|
required: true
|
|
type: choice
|
|
options:
|
|
- patch
|
|
- minor
|
|
- major
|
|
|
|
permissions:
|
|
contents: write
|
|
packages: write
|
|
|
|
concurrency:
|
|
group: release-main
|
|
cancel-in-progress: false
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Job dependency graph
|
|
#
|
|
# prepare-release
|
|
# │
|
|
# ├─── create-release
|
|
# │ │
|
|
# │ ┌────┴────────────┐
|
|
# │ │ │
|
|
# │ build-desktop build-docker
|
|
# │ (matrix: macOS, (GHCR image)
|
|
# │ linux, windows)
|
|
# │ │ │
|
|
# │ └────┬────────────┘
|
|
# │ │
|
|
# │ publish-release
|
|
# │ │
|
|
# │ notify-discord
|
|
# │
|
|
# └─── cleanup-failed-release (on failure)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
jobs:
|
|
# =========================================================================
|
|
# Phase 1: Version bump, commit, tag
|
|
# =========================================================================
|
|
prepare-release:
|
|
name: Prepare release commit and tag
|
|
runs-on: ubuntu-latest
|
|
environment: Production
|
|
outputs:
|
|
version: ${{ steps.bump.outputs.version }}
|
|
tag: ${{ steps.bump.outputs.tag }}
|
|
sha: ${{ steps.push.outputs.sha }}
|
|
steps:
|
|
- name: Enforce main branch
|
|
if: github.ref != 'refs/heads/main'
|
|
run: |
|
|
echo "This workflow can only run from main. Current ref: $GITHUB_REF"
|
|
exit 1
|
|
|
|
- name: Generate GitHub App token
|
|
id: app-token
|
|
uses: tibdex/github-app-token@v1
|
|
with:
|
|
app_id: ${{ secrets.XGITHUB_APP_ID }}
|
|
private_key: ${{ secrets.XGITHUB_APP_PRIVATE_KEY }}
|
|
|
|
- name: Checkout main
|
|
uses: actions/checkout@v4
|
|
with:
|
|
ref: main
|
|
fetch-depth: 0
|
|
token: ${{ steps.app-token.outputs.token }}
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@v4
|
|
with:
|
|
node-version: 24.x
|
|
|
|
- name: Configure Git
|
|
env:
|
|
APP_TOKEN: ${{ steps.app-token.outputs.token }}
|
|
run: |
|
|
git config user.name "github-actions[bot]"
|
|
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
git remote set-url origin https://${APP_TOKEN}@github.com/${GITHUB_REPOSITORY}.git
|
|
git fetch origin --tags
|
|
git checkout main
|
|
git pull origin main --ff-only
|
|
|
|
- name: Compute next version and sync release files
|
|
id: bump
|
|
run: node scripts/release/bump-version.js "${{ inputs.release_type }}"
|
|
|
|
- name: Ensure tag does not already exist
|
|
env:
|
|
TAG: ${{ steps.bump.outputs.tag }}
|
|
run: |
|
|
if git rev-parse "$TAG" >/dev/null 2>&1; then
|
|
echo "Tag already exists locally: $TAG"
|
|
exit 1
|
|
fi
|
|
|
|
if git ls-remote --tags origin "refs/tags/$TAG" | grep -q .; then
|
|
echo "Tag already exists on origin: $TAG"
|
|
exit 1
|
|
fi
|
|
|
|
- name: Commit, push and tag
|
|
id: push
|
|
env:
|
|
VERSION: ${{ steps.bump.outputs.version }}
|
|
TAG: ${{ steps.bump.outputs.tag }}
|
|
run: |
|
|
git add app/package.json app/src-tauri/tauri.conf.json app/src-tauri/Cargo.toml
|
|
git commit -m "chore(release): v${VERSION}"
|
|
git push origin main
|
|
|
|
git tag -a "$TAG" -m "Release $TAG"
|
|
git push origin "$TAG"
|
|
|
|
echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
|
|
|
|
# =========================================================================
|
|
# Phase 2: Create draft GitHub release
|
|
# =========================================================================
|
|
create-release:
|
|
name: Create GitHub release
|
|
runs-on: ubuntu-latest
|
|
environment: Production
|
|
needs: prepare-release
|
|
outputs:
|
|
release_id: ${{ steps.create.outputs.release_id }}
|
|
upload_url: ${{ steps.create.outputs.upload_url }}
|
|
steps:
|
|
- name: Create draft release with generated notes
|
|
id: create
|
|
uses: actions/github-script@v7
|
|
with:
|
|
script: |
|
|
const tag = '${{ needs.prepare-release.outputs.tag }}';
|
|
const version = '${{ needs.prepare-release.outputs.version }}';
|
|
const target = '${{ needs.prepare-release.outputs.sha }}';
|
|
|
|
const { owner, repo } = context.repo;
|
|
|
|
try {
|
|
await github.rest.repos.getReleaseByTag({ owner, repo, tag });
|
|
core.setFailed(`Release already exists for ${tag}`);
|
|
return;
|
|
} catch (error) {
|
|
if (error.status !== 404) {
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
const release = await github.rest.repos.createRelease({
|
|
owner,
|
|
repo,
|
|
tag_name: tag,
|
|
target_commitish: target,
|
|
name: `OpenHuman v${version}`,
|
|
draft: true,
|
|
prerelease: false,
|
|
generate_release_notes: true,
|
|
});
|
|
|
|
core.setOutput('release_id', String(release.data.id));
|
|
core.setOutput('upload_url', release.data.upload_url);
|
|
|
|
# =========================================================================
|
|
# Phase 3a: Build desktop artifacts (parallel matrix)
|
|
# =========================================================================
|
|
build-desktop:
|
|
name: "Desktop: ${{ matrix.settings.artifact_suffix }}"
|
|
needs: [prepare-release, create-release]
|
|
runs-on: ${{ matrix.settings.platform }}
|
|
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-22.04
|
|
args: --target x86_64-unknown-linux-gnu
|
|
target: x86_64-unknown-linux-gnu
|
|
artifact_suffix: ubuntu
|
|
# - 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 }}
|
|
steps:
|
|
- name: Checkout tag
|
|
uses: actions/checkout@v4
|
|
with:
|
|
ref: ${{ needs.prepare-release.outputs.tag }}
|
|
fetch-depth: 1
|
|
submodules: true
|
|
|
|
- name: Set Xcode version
|
|
if: matrix.settings.platform == 'macos-latest'
|
|
uses: maxim-lobanov/setup-xcode@v1
|
|
with:
|
|
xcode-version: latest-stable
|
|
|
|
- name: Setup Node.js 24.x
|
|
uses: actions/setup-node@v4
|
|
with:
|
|
node-version: 24.x
|
|
cache: yarn
|
|
|
|
- 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: matrix.settings.platform == 'ubuntu-22.04'
|
|
run: |
|
|
sudo apt-get update
|
|
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf cmake
|
|
|
|
# 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@v4
|
|
with:
|
|
path: |
|
|
~/.cargo/registry
|
|
~/.cargo/git
|
|
key: ${{ runner.os }}-cargo-registry-${{ steps.cargo-lock-fingerprint.outputs.hash }}
|
|
restore-keys: |
|
|
${{ runner.os }}-cargo-registry-
|
|
|
|
- name: Install dependencies
|
|
run: yarn install --frozen-lockfile
|
|
|
|
- name: Validate signing prerequisites
|
|
shell: bash
|
|
env:
|
|
MATRIX_PLATFORM: ${{ matrix.settings.platform }}
|
|
UPDATER_PUBLIC_KEY: ${{ secrets.UPDATER_PUBLIC_KEY || vars.UPDATER_PUBLIC_KEY }}
|
|
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 }}
|
|
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
|
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
|
run: |
|
|
if [ -z "$UPDATER_PUBLIC_KEY" ]; then
|
|
echo "Missing UPDATER_PUBLIC_KEY (set as secret or repo/environment variable)."
|
|
exit 1
|
|
fi
|
|
|
|
if [ -z "$TAURI_SIGNING_PRIVATE_KEY" ]; then
|
|
echo "Missing TAURI_SIGNING_PRIVATE_KEY (or fallback UPDATER_PRIVATE_KEY)."
|
|
exit 1
|
|
fi
|
|
|
|
if [ "$MATRIX_PLATFORM" = "macos-latest" ]; 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
|
|
id: config-overrides
|
|
uses: actions/github-script@v7
|
|
env:
|
|
BASE_URL: ${{ vars.BASE_URL }}
|
|
UPDATER_PUBLIC_KEY: ${{ secrets.UPDATER_PUBLIC_KEY || vars.UPDATER_PUBLIC_KEY }}
|
|
UPDATER_ENDPOINT: ${{ vars.UPDATER_ENDPOINT }}
|
|
UPDATER_REPO: tinyhumansai/openhuman
|
|
WITH_UPDATER: "true"
|
|
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));
|
|
|
|
- name: Build frontend
|
|
run: yarn build
|
|
env:
|
|
NODE_ENV: production
|
|
VITE_BACKEND_URL: ${{ vars.BASE_URL }}
|
|
VITE_SENTRY_DSN: ${{ vars.VITE_SENTRY_DSN }}
|
|
VITE_DEBUG: ${{ vars.VITE_DEBUG }}
|
|
|
|
- name: Resolve core manifest and binary names
|
|
id: core-paths
|
|
shell: bash
|
|
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"
|
|
# Cargo workspace: release artifacts are under repo root target/, not <member>/target/
|
|
echo "core_target_dir=target/$MATRIX_TARGET/release" >> "$GITHUB_OUTPUT"
|
|
echo "core_bin_name=$CORE_BIN_NAME" >> "$GITHUB_OUTPUT"
|
|
echo "sidecar_base=$SIDE_CAR_BASE" >> "$GITHUB_OUTPUT"
|
|
env:
|
|
MATRIX_TARGET: ${{ matrix.settings.target }}
|
|
|
|
- name: Build sidecar core binary
|
|
shell: bash
|
|
run: |
|
|
cargo build \
|
|
--manifest-path "$CORE_MANIFEST" \
|
|
--release \
|
|
--target "$MATRIX_TARGET" \
|
|
--bin "$CORE_BIN_NAME"
|
|
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_SENTRY_DSN: ${{ vars.OPENHUMAN_SENTRY_DSN }}
|
|
|
|
- name: Stage sidecar for Tauri bundler
|
|
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 }}"
|
|
|
|
- name: Resolve standalone core CLI artifact path
|
|
id: cli-paths
|
|
shell: bash
|
|
run: |
|
|
BASE_DIR="$CORE_TARGET_DIR"
|
|
EXE_SUFFIX=""
|
|
if [[ "$MATRIX_TARGET" == *"windows"* ]]; then
|
|
EXE_SUFFIX=".exe"
|
|
fi
|
|
echo "base_dir=$BASE_DIR" >> "$GITHUB_OUTPUT"
|
|
echo "cli_path=$BASE_DIR/${CORE_BIN_NAME}${EXE_SUFFIX}" >> "$GITHUB_OUTPUT"
|
|
env:
|
|
MATRIX_TARGET: ${{ matrix.settings.target }}
|
|
CORE_TARGET_DIR: ${{ steps.core-paths.outputs.core_target_dir }}
|
|
CORE_BIN_NAME: ${{ steps.core-paths.outputs.core_bin_name }}
|
|
|
|
- name: Build, package and upload to release
|
|
uses: tauri-apps/tauri-action@v0.6.2
|
|
id: tauri-build
|
|
env:
|
|
# NOTE: APPLE_CERTIFICATE, APPLE_SIGNING_IDENTITY, APPLE_ID, etc. are
|
|
# intentionally omitted so tauri-action builds WITHOUT signing.
|
|
# Signing + notarization are handled in a dedicated step afterwards
|
|
# where we sign everything with hardened runtime + entitlements
|
|
# (required by Apple notarization).
|
|
BASE_URL: ${{ vars.BASE_URL }}
|
|
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: "true"
|
|
with:
|
|
projectPath: app
|
|
args: -c ${{ steps.config-overrides.outputs.json }} ${{ matrix.settings.args }}
|
|
includeDebug: false
|
|
includeRelease: true
|
|
releaseId: ${{ needs.create-release.outputs.release_id }}
|
|
owner: tinyhumansai
|
|
repo: openhuman
|
|
|
|
- name: Locate macOS .app bundle
|
|
if: matrix.settings.platform == 'macos-latest'
|
|
id: locate-app
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
# tauri-action with projectPath: app may output to either location
|
|
# depending on workspace config — search both
|
|
APP_PATH=""
|
|
for candidate in \
|
|
"app/src-tauri/target/${{ matrix.settings.target }}/release/bundle/macos/OpenHuman.app" \
|
|
"target/${{ matrix.settings.target }}/release/bundle/macos/OpenHuman.app"; do
|
|
if [ -d "$candidate" ]; then
|
|
APP_PATH="$candidate"
|
|
break
|
|
fi
|
|
done
|
|
|
|
# Fallback: search for it
|
|
if [ -z "$APP_PATH" ]; then
|
|
APP_PATH="$(find . -path '*/release/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: Re-sign sidecar with hardened runtime and notarize
|
|
if: 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 }}
|
|
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: matrix.settings.platform == 'macos-latest'
|
|
shell: bash
|
|
env:
|
|
APPLE_ID: ${{ secrets.APPLE_ID }}
|
|
APPLE_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_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: matrix.settings.platform == 'macos-latest'
|
|
shell: bash
|
|
env:
|
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
RELEASE_ID: ${{ needs.create-release.outputs.release_id }}
|
|
run: |
|
|
bash scripts/release/upload-macos-artifacts.sh \
|
|
"${{ steps.locate-app.outputs.app_path }}" \
|
|
"${{ steps.locate-app.outputs.bundle_dir }}" \
|
|
"${{ needs.prepare-release.outputs.version }}" \
|
|
"${{ matrix.settings.target }}"
|
|
|
|
- name: Verify macOS app bundle sidecar layout
|
|
if: matrix.settings.platform == 'macos-latest'
|
|
shell: bash
|
|
run: |
|
|
APP_PATH="${{ steps.locate-app.outputs.app_path }}"
|
|
echo "Inspecting bundle at: $APP_PATH"
|
|
ls -la "$APP_PATH/Contents/MacOS"
|
|
ls -la "$APP_PATH/Contents/Resources" | grep openhuman || true
|
|
|
|
# Sidecar may be in MacOS/ or Resources/ depending on Tauri version
|
|
FOUND=false
|
|
if ls "$APP_PATH/Contents/MacOS"/openhuman* 2>/dev/null | grep -qv OpenHuman; then
|
|
echo "Sidecar found in Contents/MacOS/"
|
|
FOUND=true
|
|
fi
|
|
if ls "$APP_PATH/Contents/Resources"/openhuman-* 2>/dev/null; then
|
|
echo "Sidecar found in Contents/Resources/"
|
|
FOUND=true
|
|
fi
|
|
if [ "$FOUND" = "false" ]; then
|
|
echo "WARNING: Sidecar binary not found in expected locations"
|
|
fi
|
|
|
|
# Verify notarization staple
|
|
echo "Checking staple..."
|
|
xcrun stapler validate "$APP_PATH" || echo "WARNING: Staple validation failed"
|
|
|
|
- name: Package CLI tarball and upload to release
|
|
if: matrix.settings.platform != 'windows-latest'
|
|
shell: bash
|
|
env:
|
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
run: |
|
|
# For macOS, prefer the notarized binary from inside the .app bundle
|
|
if [[ "${{ matrix.settings.platform }}" == "macos-latest" ]]; then
|
|
APP_PATH="${{ steps.locate-app.outputs.app_path }}"
|
|
BIN=$(find "$APP_PATH/Contents/MacOS" -maxdepth 1 -name "openhuman-core-*" \
|
|
! -name "*.sig" 2>/dev/null | head -1 || true)
|
|
[[ -z "$BIN" ]] && BIN=$(find "$APP_PATH/Contents/Resources" -maxdepth 1 \
|
|
-name "openhuman-core-*" ! -name "*.sig" 2>/dev/null | head -1 || true)
|
|
if [[ -z "$BIN" ]]; then
|
|
echo "[pkg] Falling back to target dir binary (no notarized sidecar found)"
|
|
BIN="${{ steps.cli-paths.outputs.cli_path }}"
|
|
fi
|
|
else
|
|
BIN="${{ steps.cli-paths.outputs.cli_path }}"
|
|
fi
|
|
|
|
bash scripts/release/package-cli-tarball.sh \
|
|
"$BIN" \
|
|
"${{ needs.prepare-release.outputs.version }}" \
|
|
"${{ matrix.settings.target }}"
|
|
|
|
- name: Upload standalone CLI artifacts
|
|
uses: actions/upload-artifact@v4
|
|
with:
|
|
name: standalone-bins-${{ matrix.settings.platform }}-${{ matrix.settings.artifact_suffix }}
|
|
path: |
|
|
${{ steps.cli-paths.outputs.cli_path }}
|
|
|
|
# =========================================================================
|
|
# Phase 3b: Build & push Docker image (runs parallel with build-desktop)
|
|
# =========================================================================
|
|
build-docker:
|
|
name: "Docker: build and push"
|
|
needs: [prepare-release, create-release]
|
|
runs-on: ubuntu-latest
|
|
environment: Production
|
|
env:
|
|
REGISTRY: ghcr.io
|
|
IMAGE_NAME: tinyhumansai/openhuman-core
|
|
steps:
|
|
- name: Checkout tag
|
|
uses: actions/checkout@v4
|
|
with:
|
|
ref: ${{ needs.prepare-release.outputs.tag }}
|
|
fetch-depth: 1
|
|
|
|
- name: Set up Docker Buildx
|
|
uses: docker/setup-buildx-action@v3
|
|
|
|
- name: Log in to GHCR
|
|
uses: docker/login-action@v3
|
|
with:
|
|
registry: ${{ env.REGISTRY }}
|
|
username: ${{ github.actor }}
|
|
password: ${{ secrets.GITHUB_TOKEN }}
|
|
|
|
- name: Build and push staging tag
|
|
uses: docker/build-push-action@v6
|
|
with:
|
|
context: .
|
|
file: Dockerfile
|
|
push: true
|
|
tags: |
|
|
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:staging-${{ needs.prepare-release.outputs.tag }}
|
|
cache-from: type=gha
|
|
cache-to: type=gha,mode=max
|
|
labels: |
|
|
org.opencontainers.image.title=openhuman-core
|
|
org.opencontainers.image.description=OpenHuman core JSON-RPC server
|
|
org.opencontainers.image.version=${{ needs.prepare-release.outputs.version }}
|
|
org.opencontainers.image.source=https://github.com/tinyhumansai/openhuman
|
|
org.opencontainers.image.revision=${{ needs.prepare-release.outputs.sha }}
|
|
|
|
# =========================================================================
|
|
# Phase 4: Publish the draft release (waits for ALL build phases)
|
|
# =========================================================================
|
|
publish-release:
|
|
name: Publish draft release
|
|
runs-on: ubuntu-latest
|
|
environment: Production
|
|
needs: [prepare-release, create-release, build-desktop, build-docker]
|
|
if: needs.build-desktop.result == 'success' && needs.build-docker.result == 'success'
|
|
steps:
|
|
- name: Validate required installer assets exist
|
|
uses: actions/github-script@v7
|
|
with:
|
|
script: |
|
|
const releaseId = Number('${{ needs.create-release.outputs.release_id }}');
|
|
const { owner, repo } = context.repo;
|
|
const { data: assets } = await github.rest.repos.listReleaseAssets({
|
|
owner,
|
|
repo,
|
|
release_id: releaseId,
|
|
per_page: 100,
|
|
});
|
|
|
|
const names = assets.map((a) => a.name);
|
|
const requiredPatterns = [
|
|
/OpenHuman_.*_aarch64\.dmg$/,
|
|
/OpenHuman_.*_x64\.dmg$/,
|
|
/OpenHuman_.*_amd64\.AppImage$/,
|
|
/OpenHuman_.*_amd64\.deb$/,
|
|
// Windows build is currently disabled in the matrix
|
|
// /(OpenHuman_.*_x64-setup\.exe$|OpenHuman_.*_x64.*\.msi$)/,
|
|
];
|
|
|
|
const missing = requiredPatterns.filter((pattern) => !names.some((name) => pattern.test(name)));
|
|
if (missing.length > 0) {
|
|
core.setFailed(`Missing required installer assets. Got: ${names.join(', ')}`);
|
|
return;
|
|
}
|
|
core.info('All required installer assets are present.');
|
|
|
|
- name: Promote Docker image from staging to release tags
|
|
env:
|
|
REGISTRY: ghcr.io
|
|
IMAGE_NAME: tinyhumansai/openhuman-core
|
|
TAG: ${{ needs.prepare-release.outputs.tag }}
|
|
run: |
|
|
# Log in to GHCR
|
|
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u "${{ github.actor }}" --password-stdin
|
|
|
|
STAGING="${REGISTRY}/${IMAGE_NAME}:staging-${TAG}"
|
|
VERSIONED="${REGISTRY}/${IMAGE_NAME}:${TAG}"
|
|
LATEST="${REGISTRY}/${IMAGE_NAME}:latest"
|
|
|
|
echo "Promoting ${STAGING} → ${VERSIONED}, ${LATEST}"
|
|
docker buildx imagetools create --tag "${VERSIONED}" --tag "${LATEST}" "${STAGING}"
|
|
|
|
- name: Append Docker pull instructions to release notes
|
|
env:
|
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
TAG: ${{ needs.prepare-release.outputs.tag }}
|
|
IMAGE_NAME: tinyhumansai/openhuman-core
|
|
run: |
|
|
gh release view "${TAG}" --repo tinyhumansai/openhuman --json body -q .body > /tmp/body.md
|
|
printf '\n---\n\n### Docker\n\n```\ndocker pull ghcr.io/%s:%s\ndocker run -p 7788:7788 --env-file .env ghcr.io/%s:%s\n```\n' \
|
|
"${IMAGE_NAME}" "${TAG}" "${IMAGE_NAME}" "${TAG}" >> /tmp/body.md
|
|
gh release edit "${TAG}" --repo tinyhumansai/openhuman --notes-file /tmp/body.md
|
|
|
|
- name: Publish release
|
|
uses: actions/github-script@v7
|
|
with:
|
|
script: |
|
|
const releaseId = Number('${{ needs.create-release.outputs.release_id }}');
|
|
await github.rest.repos.updateRelease({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
release_id: releaseId,
|
|
draft: false,
|
|
});
|
|
core.info(`Published release ${releaseId}`);
|
|
|
|
# =========================================================================
|
|
# Cleanup: remove draft release + tag if ANY build phase failed
|
|
# =========================================================================
|
|
cleanup-failed-release:
|
|
name: Remove release and tag if build failed
|
|
runs-on: ubuntu-latest
|
|
environment: Production
|
|
needs: [prepare-release, create-release, build-desktop, build-docker]
|
|
if: >-
|
|
always()
|
|
&& needs.create-release.result == 'success'
|
|
&& (needs.build-desktop.result == 'failure' || needs.build-desktop.result == 'cancelled'
|
|
|| needs.build-docker.result == 'failure' || needs.build-docker.result == 'cancelled')
|
|
steps:
|
|
- name: Delete GitHub release
|
|
uses: actions/github-script@v7
|
|
with:
|
|
script: |
|
|
const owner = context.repo.owner;
|
|
const repo = context.repo.repo;
|
|
const releaseId = Number('${{ needs.create-release.outputs.release_id }}');
|
|
|
|
if (!Number.isFinite(releaseId) || releaseId <= 0) {
|
|
core.setFailed('Invalid or missing release_id; cannot delete release.');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await github.rest.repos.deleteRelease({ owner, repo, release_id: releaseId });
|
|
core.info(`Deleted release ${releaseId}`);
|
|
} catch (e) {
|
|
core.warning(`deleteRelease failed: ${e.message}`);
|
|
}
|
|
|
|
- name: Delete remote tag
|
|
uses: actions/github-script@v7
|
|
with:
|
|
script: |
|
|
const owner = context.repo.owner;
|
|
const repo = context.repo.repo;
|
|
const tag = '${{ needs.prepare-release.outputs.tag }}';
|
|
|
|
try {
|
|
await github.rest.git.deleteRef({ owner, repo, ref: `tags/${tag}` });
|
|
core.info(`Deleted remote tag ${tag}`);
|
|
} catch (e) {
|
|
if (e.status === 404) {
|
|
core.info(`Tag ${tag} already absent on remote`);
|
|
} else {
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
- name: Delete staging Docker image
|
|
continue-on-error: true
|
|
env:
|
|
TAG: ${{ needs.prepare-release.outputs.tag }}
|
|
run: |
|
|
PACKAGE="openhuman-core"
|
|
STAGING_TAG="staging-${TAG}"
|
|
echo "Attempting to delete staging Docker tag: ${STAGING_TAG}"
|
|
# List package versions and find the staging tag
|
|
VERSION_ID="$(gh api \
|
|
-H "Accept: application/vnd.github+json" \
|
|
"/orgs/tinyhumansai/packages/container/${PACKAGE}/versions" \
|
|
--paginate --jq ".[] | select(.metadata.container.tags[] == \"${STAGING_TAG}\") | .id" 2>/dev/null | head -1)"
|
|
if [ -n "$VERSION_ID" ]; then
|
|
gh api -X DELETE "/orgs/tinyhumansai/packages/container/${PACKAGE}/versions/${VERSION_ID}" || true
|
|
echo "Deleted staging image version ${VERSION_ID}"
|
|
else
|
|
echo "Staging tag ${STAGING_TAG} not found or already deleted"
|
|
fi
|