mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-28 13:32:23 +00:00
* fix(chat): keep in-flight responses alive across tab switches Made-with: Cursor * chore: satisfy lint and format push gates Made-with: Cursor * fix: address CodeRabbit review on chat runtime PR Made-with: Cursor * feat(chat): add explicit inference turn lifecycle in chat runtime slice Made-with: Cursor * feat(chat): implement thinking summary feature in chat responses Added a new mechanism to accumulate and send model reasoning text as a separate message during chat interactions. This includes handling "thinking_delta" events to gather reasoning content, formatting it for clarity, and ensuring it is sent before the main response. Updated the StreamingState struct to include a thinking accumulator for this purpose. This enhancement improves user experience by providing insight into the model's reasoning process. * feat(telegram): update bot username handling for staging and production environments Refactored the Telegram bot username resolution logic to differentiate between staging and production environments. Introduced constants for default usernames based on the application environment and updated the GitHub Actions workflow to set the appropriate environment variables. This change enhances the flexibility and clarity of bot username management in the application. * fix: resolve CodeRabbit review issues on feat/thinking-telegram-summary - bus.rs: fix UTF-8 char boundary panic in format_thinking_summary truncation - threadSlice.ts: remove premature activeThreadId clear from addInferenceResponse.rejected - Conversations.tsx: add composerBlocked global lock, clear tool timeline in safety timeout - ChatRuntimeProvider.tsx: replace stale toolTimelineRef/inferenceStatusRef reads with live store.getState() calls in all event handlers - LocalAIDownloadSnackbar.tsx: reset dismissed/collapsed on not-downloading → downloading transition edge - MemoryGraphMap.tsx: derive activeSelectedNode to guard stale selectedNode after relations refresh; add debug logs to useMemo graph recompute Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: use render-phase update for download dismiss reset in LocalAIDownloadSnackbar Replace effect-based setState with the React render-phase update pattern so the not-downloading → downloading transition resets dismissed/collapsed without triggering the react-hooks/set-state-in-effect lint warning. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: apply prettier and cargo fmt auto-formatting Formatting changes applied by the pre-push hook during the previous commit. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor: replace endInferenceTurn with clearRuntimeForThread in Conversations component Updated the Conversations component to replace the endInferenceTurn dispatch with clearRuntimeForThread. This change simplifies the handling of thread runtime state during error scenarios and timeout conditions, ensuring a cleaner state management approach. --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
880 lines
36 KiB
YAML
880 lines
36 KiB
YAML
name: Release
|
|
|
|
on:
|
|
workflow_dispatch:
|
|
inputs:
|
|
build_target:
|
|
description: Build target environment
|
|
required: true
|
|
type: choice
|
|
default: production
|
|
options:
|
|
- production
|
|
- staging
|
|
release_type:
|
|
description: Version increment type
|
|
required: false
|
|
default: patch
|
|
type: choice
|
|
options:
|
|
- patch
|
|
- minor
|
|
- major
|
|
|
|
permissions:
|
|
contents: write
|
|
packages: write
|
|
|
|
concurrency:
|
|
group: release-${{ github.event.inputs.build_target || 'production' }}-main
|
|
cancel-in-progress: false
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Job dependency graph
|
|
#
|
|
# prepare-build
|
|
# │
|
|
# ├─── 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-build:
|
|
name: Prepare build context
|
|
runs-on: ubuntu-latest
|
|
environment: Production
|
|
outputs:
|
|
version: ${{ steps.resolve.outputs.version }}
|
|
tag: ${{ steps.resolve.outputs.tag }}
|
|
sha: ${{ steps.resolve.outputs.sha }}
|
|
build_ref: ${{ steps.resolve.outputs.build_ref }}
|
|
release_enabled: ${{ steps.resolve.outputs.release_enabled }}
|
|
base_url: ${{ steps.resolve.outputs.base_url }}
|
|
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
|
|
if: inputs.build_target == 'production'
|
|
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 for production release
|
|
if: inputs.build_target == 'production'
|
|
uses: actions/checkout@v4
|
|
with:
|
|
ref: main
|
|
fetch-depth: 0
|
|
token: ${{ steps.app-token.outputs.token }}
|
|
|
|
- name: Checkout main for staging build
|
|
if: inputs.build_target == 'staging'
|
|
uses: actions/checkout@v4
|
|
with:
|
|
ref: main
|
|
fetch-depth: 0
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@v4
|
|
with:
|
|
node-version: 24.x
|
|
|
|
- name: Configure Git
|
|
if: inputs.build_target == 'production'
|
|
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
|
|
if: inputs.build_target == 'production'
|
|
id: bump
|
|
run: node scripts/release/bump-version.js "${{ inputs.release_type }}"
|
|
|
|
- name: Verify release version sync
|
|
if: inputs.build_target == 'production'
|
|
run: node scripts/release/verify-version-sync.js "${{ steps.bump.outputs.version }}"
|
|
|
|
- name: Ensure tag does not already exist
|
|
if: inputs.build_target == 'production'
|
|
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
|
|
if: inputs.build_target == 'production'
|
|
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 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"
|
|
|
|
- name: Resolve build outputs
|
|
id: resolve
|
|
shell: bash
|
|
run: |
|
|
if [ "${{ inputs.build_target }}" = "production" ]; then
|
|
VERSION="${{ steps.bump.outputs.version }}"
|
|
TAG="${{ steps.bump.outputs.tag }}"
|
|
SHA="${{ steps.push.outputs.sha }}"
|
|
BUILD_REF="$TAG"
|
|
RELEASE_ENABLED="true"
|
|
BASE_URL="https://api.tinyhumans.ai/"
|
|
else
|
|
VERSION="$(node -p "require('./app/package.json').version")"
|
|
TAG=""
|
|
SHA="$(git rev-parse HEAD)"
|
|
BUILD_REF="$SHA"
|
|
RELEASE_ENABLED="false"
|
|
BASE_URL="https://staging-api.tinyhumans.ai/"
|
|
fi
|
|
|
|
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
|
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
|
|
echo "sha=$SHA" >> "$GITHUB_OUTPUT"
|
|
echo "build_ref=$BUILD_REF" >> "$GITHUB_OUTPUT"
|
|
echo "release_enabled=$RELEASE_ENABLED" >> "$GITHUB_OUTPUT"
|
|
echo "base_url=$BASE_URL" >> "$GITHUB_OUTPUT"
|
|
|
|
# =========================================================================
|
|
# Phase 2: Create draft GitHub release
|
|
# =========================================================================
|
|
create-release:
|
|
name: Create GitHub release
|
|
runs-on: ubuntu-latest
|
|
environment: Production
|
|
if: needs.prepare-build.outputs.release_enabled == 'true'
|
|
needs: prepare-build
|
|
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-build.outputs.tag }}';
|
|
const version = '${{ needs.prepare-build.outputs.version }}';
|
|
const target = '${{ needs.prepare-build.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-build, create-release]
|
|
if: >-
|
|
always()
|
|
&& needs.prepare-build.result == 'success'
|
|
&& (needs.prepare-build.outputs.release_enabled != 'true'
|
|
|| needs.create-release.result == 'success')
|
|
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 }}
|
|
# Keep in sync with DEFAULT_TELEGRAM_BOT_USERNAME_* in channels/controllers/ops.rs
|
|
OPENHUMAN_TELEGRAM_BOT_USERNAME: ${{ inputs.build_target == 'staging' && 'alphahumantest_bot' || 'openhumanaibot' }}
|
|
VITE_TELEGRAM_BOT_USERNAME: ${{ inputs.build_target == 'staging' && 'alphahumantest_bot' || 'openhumanaibot' }}
|
|
steps:
|
|
- name: Checkout build ref
|
|
uses: actions/checkout@v4
|
|
with:
|
|
ref: ${{ needs.prepare-build.outputs.build_ref }}
|
|
fetch-depth: 1
|
|
submodules: true
|
|
|
|
- name: Configure staging app environment
|
|
if: inputs.build_target == 'staging'
|
|
shell: bash
|
|
run: |
|
|
echo "OPENHUMAN_APP_ENV=staging" >> "$GITHUB_ENV"
|
|
echo "VITE_OPENHUMAN_APP_ENV=staging" >> "$GITHUB_ENV"
|
|
echo "OPENHUMAN_TELEGRAM_BOT_USERNAME=alphahumantest_bot" >> "$GITHUB_ENV"
|
|
echo "VITE_TELEGRAM_BOT_USERNAME=alphahumantest_bot" >> "$GITHUB_ENV"
|
|
|
|
- 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 libasound2-dev libxdo-dev libxtst-dev libx11-dev libxi-dev libevdev-dev libssl-dev libclang-dev
|
|
|
|
# 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: ${{ needs.prepare-build.outputs.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_OPENHUMAN_APP_ENV: ${{ inputs.build_target == 'staging' && 'staging' || 'production' }}
|
|
VITE_BACKEND_URL: ${{ needs.prepare-build.outputs.base_url }}
|
|
VITE_SENTRY_DSN: ${{ vars.VITE_SENTRY_DSN }}
|
|
VITE_DEBUG: ${{ vars.VITE_DEBUG }}
|
|
# OAuth guardrails (#365): block openhuman://oauth/success on outdated desktop builds.
|
|
VITE_MINIMUM_SUPPORTED_APP_VERSION: ${{ vars.VITE_MINIMUM_SUPPORTED_APP_VERSION }}
|
|
VITE_LATEST_APP_DOWNLOAD_URL: ${{ vars.VITE_LATEST_APP_DOWNLOAD_URL }}
|
|
|
|
- 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_APP_ENV: ${{ inputs.build_target == 'staging' && 'staging' || 'production' }}
|
|
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
|
|
if: needs.prepare-build.outputs.release_enabled == 'true'
|
|
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: ${{ needs.prepare-build.outputs.base_url }}
|
|
OPENHUMAN_APP_ENV: ${{ inputs.build_target == 'staging' && 'staging' || 'production' }}
|
|
VITE_OPENHUMAN_APP_ENV: ${{ inputs.build_target == 'staging' && 'staging' || 'production' }}
|
|
VITE_BACKEND_URL: ${{ needs.prepare-build.outputs.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"
|
|
# Must match standalone "Build frontend" so tauri-action's beforeBuildCommand embeds the gate.
|
|
VITE_MINIMUM_SUPPORTED_APP_VERSION: ${{ vars.VITE_MINIMUM_SUPPORTED_APP_VERSION }}
|
|
VITE_LATEST_APP_DOWNLOAD_URL: ${{ vars.VITE_LATEST_APP_DOWNLOAD_URL }}
|
|
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: Build and package staging artifacts
|
|
if: needs.prepare-build.outputs.release_enabled != 'true'
|
|
shell: bash
|
|
env:
|
|
BASE_URL: ${{ needs.prepare-build.outputs.base_url }}
|
|
OPENHUMAN_APP_ENV: staging
|
|
VITE_OPENHUMAN_APP_ENV: staging
|
|
VITE_BACKEND_URL: ${{ needs.prepare-build.outputs.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"
|
|
VITE_MINIMUM_SUPPORTED_APP_VERSION: ${{ vars.VITE_MINIMUM_SUPPORTED_APP_VERSION }}
|
|
VITE_LATEST_APP_DOWNLOAD_URL: ${{ vars.VITE_LATEST_APP_DOWNLOAD_URL }}
|
|
run: |
|
|
yarn --cwd app tauri build \
|
|
-c '${{ steps.config-overrides.outputs.json }}' \
|
|
${{ matrix.settings.args }}
|
|
|
|
- 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' && needs.prepare-build.outputs.release_enabled == 'true'
|
|
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-build.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 (unix)
|
|
if: matrix.settings.platform != 'windows-latest' && needs.prepare-build.outputs.release_enabled == 'true'
|
|
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-build.outputs.version }}" \
|
|
"${{ matrix.settings.target }}"
|
|
|
|
- name: Package CLI zip and upload to release (windows)
|
|
if: matrix.settings.platform == 'windows-latest' && needs.prepare-build.outputs.release_enabled == 'true'
|
|
shell: pwsh
|
|
env:
|
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
run: |
|
|
$Version = "${{ needs.prepare-build.outputs.version }}"
|
|
$Target = "${{ matrix.settings.target }}"
|
|
$BinPath = "${{ steps.cli-paths.outputs.cli_path }}"
|
|
$ZipName = "openhuman-core-${Version}-${Target}.zip"
|
|
|
|
$Work = New-Item -ItemType Directory -Path (Join-Path $env:TEMP "cli-pkg-$(Get-Random)")
|
|
Copy-Item $BinPath (Join-Path $Work.FullName "openhuman-core.exe")
|
|
Compress-Archive -Path (Join-Path $Work.FullName "openhuman-core.exe") -DestinationPath $ZipName
|
|
|
|
$Hash = (Get-FileHash -Path $ZipName -Algorithm SHA256).Hash.ToLowerInvariant()
|
|
Set-Content -Path "${ZipName}.sha256" -Value $Hash -NoNewline
|
|
|
|
Write-Host "[package-cli] Created $ZipName (sha256: $Hash)"
|
|
|
|
gh release upload "v${Version}" $ZipName "${ZipName}.sha256" --repo tinyhumansai/openhuman --clobber
|
|
Write-Host "[package-cli] Uploaded $ZipName to v${Version}"
|
|
|
|
- name: Upload staging desktop bundles
|
|
if: needs.prepare-build.outputs.release_enabled != 'true'
|
|
uses: actions/upload-artifact@v4
|
|
with:
|
|
name: desktop-bundles-${{ matrix.settings.platform }}-${{ matrix.settings.artifact_suffix }}
|
|
path: |
|
|
app/src-tauri/target/${{ matrix.settings.target }}/release/bundle/**
|
|
target/${{ matrix.settings.target }}/release/bundle/**
|
|
|
|
- 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-build, create-release]
|
|
if: >-
|
|
always()
|
|
&& needs.prepare-build.result == 'success'
|
|
&& needs.prepare-build.outputs.release_enabled == 'true'
|
|
&& needs.create-release.result == 'success'
|
|
runs-on: ubuntu-latest
|
|
environment: Production
|
|
env:
|
|
REGISTRY: ghcr.io
|
|
IMAGE_NAME: tinyhumansai/openhuman-core
|
|
steps:
|
|
- name: Checkout build ref
|
|
uses: actions/checkout@v4
|
|
with:
|
|
ref: ${{ needs.prepare-build.outputs.build_ref }}
|
|
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 }}
|
|
|
|
# TODO: Uncomment this when we have a staging tag
|
|
# - 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-build, create-release, build-desktop, build-docker]
|
|
if: >-
|
|
needs.prepare-build.outputs.release_enabled == 'true'
|
|
&& 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$/,
|
|
/(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-build, create-release, build-desktop, build-docker]
|
|
if: >-
|
|
always()
|
|
&& needs.prepare-build.outputs.release_enabled == 'true'
|
|
&& 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-build.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-build.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
|