Files
openhuman/.github/workflows/release.yml
T
Steven EnamakelandGitHub 5a8a7edb91 Refine billing, settings, rewards, and usage UI (#547)
* feat: add react-icons support and refactor skill category handling

- Added `react-icons` dependency to the project for enhanced icon usage.
- Introduced new skill icons in the `toolkitMeta.tsx` component, replacing SVG icons with `react-icons` for improved maintainability and consistency.
- Created a new `skillCategories.ts` file to define skill categories and their order, streamlining the management of skill categories across the application.
- Refactored the `SkillCategoryFilter` component to utilize the new skill categories structure, enhancing clarity and reducing redundancy in the codebase.
- Updated the `Skills` page to leverage the new icon rendering method and skill categories, improving the overall user experience.
- Added tests to ensure the correct functionality of the new skill category and icon implementations.

* feat: update environment configuration and enhance settings UI

- Updated `.env.example` and `app/.env.example` to reflect new backend URL and added optional environment selector for staging.
- Enhanced `SettingsHome` component by separating account and billing sections for improved clarity.
- Introduced a new billing section in the settings menu to streamline user navigation.
- Updated `useSettingsNavigation` hook to accommodate changes in settings structure.
- Improved `BillingPanel` to handle session token checks and ensure accurate billing state retrieval.
- Refactored `Rewards` page to enhance user experience with clearer progress indicators and improved layout.
- Added tests to validate changes in the rewards and settings components.

* feat(config): enhance API base URL handling and environment configuration

- Introduced a new staging API base URL constant for better environment management.
- Added app environment variable constants to streamline environment detection.
- Refactored effective API URL resolution to accommodate environment-specific defaults.
- Implemented functions to resolve app environment from process environment variables.
- Added tests to validate the correct behavior of staging and production API URL handling.

* feat(rewards): add community and referrals tabs for rewards management

- Introduced `RewardsCommunityTab` and `RewardsReferralsTab` components to enhance the rewards management interface.
- The `RewardsCommunityTab` displays user progress, Discord role statuses, and connection options, improving user engagement with rewards.
- The `RewardsReferralsTab` allows users to manage their referral program, track progress, and access coupon rewards in a streamlined layout.
- Updated the `Rewards` page to integrate these new tabs, enhancing overall user experience and navigation.

* feat(rewards): introduce ReferralRewardsSection and RewardsRedeemTab components

- Added `ReferralRewardsSection` to manage referral statistics and code application, enhancing user engagement with the referral program.
- Created `RewardsRedeemTab` to streamline the process of applying reward codes, improving the overall rewards management experience.
- Updated `Rewards` page to include the new redeem tab, allowing users to easily switch between referral and redeem functionalities.
- Refactored `RewardsCommunityTab` to adjust the referral selection handler, ensuring consistent navigation across the rewards interface.
- Removed redundant UI elements in `RewardsCouponSection` for a cleaner layout.
- Enhanced tests to cover new functionalities and ensure robust performance across the rewards system.

* feat(ui): introduce PillTabBar component and refactor SkillCategoryFilter and Rewards pages

- Added a new `PillTabBar` component to enhance tab navigation with customizable styles and item rendering.
- Refactored `SkillCategoryFilter` to utilize `PillTabBar`, improving code clarity and reducing redundancy in button rendering.
- Updated the `Rewards` page to replace the existing tab navigation with `PillTabBar`, streamlining the user interface for switching between referral, rewards, and redeem tabs.
- Enhanced the `ReferralRewardsSection` layout for better user experience and consistency across the rewards management interface.
- Improved tests to cover the new `PillTabBar` functionality and ensure robust performance across the updated components.

* fix(rewards): update placeholder and button text in RewardsCouponSection

- Changed the input placeholder from "Promo code" to "Coupon code" for clarity.
- Updated button text from "Apply code" to "Redeem Code" to better reflect the action being performed.
- Adjusted loading state text from "Applying…" to "Redeeming..." for consistency in user feedback.

* feat(composio): enhance toolkit handling and improve user messaging

- Updated the `ComposioConnectModal` to simplify the connection message, removing unnecessary wording for clarity.
- Introduced a `TOOLKIT_ALIASES` mapping in `toolkitMeta.tsx` to standardize toolkit slugs, improving consistency across the application.
- Refactored the `composioToolkitMeta` function to utilize the new slug mapping, enhancing toolkit metadata retrieval.
- Improved the `useComposioIntegrations` hook to leverage the canonicalized toolkit slugs for better integration handling.
- Adjusted the `Skills` page to normalize toolkit slugs during rendering, ensuring a consistent user experience.
- Updated tests to reflect changes in messaging and toolkit handling, ensuring robust functionality across the application.

* feat(intelligence): add new tabs for Dreams, Memory, and Subconscious features

- Introduced `IntelligenceDreamsTab`, which displays generated dreams based on daily life events, enhancing user engagement with a visually appealing layout.
- Added `IntelligenceMemoryTab` to manage actionable items, featuring search and filter capabilities for improved user interaction with memory data.
- Created `IntelligenceSubconsciousTab` to handle subconscious tasks and logs, providing users with insights and management options for their subconscious activities.
- Refactored the `RewardsCommunityTab` to remove unused Discord role status logic, streamlining the component for better performance.
- Implemented a new `PageBackButton` component for consistent navigation across settings pages.
- Enhanced the `BillingPanel` and its subcomponents to improve user experience in managing billing and subscription details, including transaction history and payment methods.
- Added new billing-related tabs for better organization and access to billing features, including `BillingOverviewTab`, `BillingPaymentsTab`, and `BillingPlansTab`.
- Updated tests to ensure functionality across new and refactored components, maintaining robust application performance.

* refactor(billing): update BillingPanel and BillingPlansTab for improved user experience

- Changed the default selected tab in `BillingPanel` from 'overview' to 'plans' to prioritize subscription management.
- Removed the `BillingOverviewTab` from the `BillingPanel`, streamlining the billing interface.
- Enhanced the `BillingPlansTab` header to clarify the purpose, changing "Explore tiers" to "Choose a Subscription Plan".
- Updated the description in `BillingPlansTab` for better clarity on payment options.
- Improved the layout of the `SubscriptionPlans` component to better highlight crypto payment options and their availability.
- Cleaned up unused code and comments for better maintainability.

* refactor(billing): streamline BillingPanel and BillingPlansTab components

- Removed unused `teamUsage` state and related API call from `BillingPanel` to simplify data management.
- Adjusted layout in `BillingPlansTab` for improved visual hierarchy and user experience.
- Cleaned up code by eliminating unnecessary comments and enhancing maintainability.

* refactor(billing): enhance SubscriptionPlans layout for improved responsiveness

- Updated the layout of the `SubscriptionPlans` component to ensure better responsiveness and visual consistency.
- Adjusted class names to include minimum height and width properties for better alignment across different screen sizes.
- Enhanced the layout of the pricing display section for improved clarity and user experience.

* refactor(billing): update subscription plans and billing components for improved clarity and user experience

- Adjusted pricing for BASIC and PRO plans to reflect new monthly and annual rates.
- Enhanced feature descriptions for subscription plans to better communicate value.
- Removed redundant UI elements in the BillingPaymentsTab and PayAsYouGoCard for a cleaner layout.
- Added loading and confirmation messages in SubscriptionPlans to improve user feedback during payment processes.
- Updated class names for better responsiveness and visual consistency across billing components.

* refactor(billing): improve error messaging and UI consistency in billing components

- Updated error message in BillingPanel to specify adding a payment card on Stripe for clarity.
- Changed header text in AutoRechargeSection to "Enable Auto-Recharge" for better user understanding.
- Modified button text in AutoRechargeSection to "Add card on Stripe" for consistency.
- Enhanced styling in PayAsYouGoCard for improved visual appeal and user interaction.
- Removed redundant UI elements in PayAsYouGoCard for a cleaner layout.

* refactor(billing): remove unused error handling and improve UI consistency across billing components

- Eliminated `setArError` prop from BillingPanel, AutoRechargeSection, and BillingPaymentsTab to streamline error handling.
- Enhanced layout and styling in BillingHistoryTab and SubscriptionPlans for better visual consistency and user experience.
- Removed the BillingOverviewTab component to simplify the billing interface and improve maintainability.

* refactor(billing): update feature descriptions and enhance SubscriptionPlans display

- Revised feature descriptions in the PLANS array for clarity and conciseness.
- Increased the number of displayed features in the SubscriptionPlans component to provide users with more information at a glance.

* refactor(billing): update billing components for improved clarity and functionality

- Revised feature descriptions in the PLANS array to reflect increased usage limits.
- Renamed header in BillingHistoryTab to "Transaction History" and updated description for clarity.
- Adjusted transaction amount formatting in BillingHistoryTab to display five decimal places.
- Removed redundant UI elements in BillingPaymentsTab to streamline the layout.
- Enhanced PayAsYouGoCard with improved credit balance display and top-up options, including validation for custom amounts.

* feat(usage): add budget completion message logic and update tests

- Introduced `shouldShowBudgetCompletedMessage` to the `UsageState` interface to indicate when the budget completion message should be displayed.
- Updated the `useUsageState` hook to calculate the budget completion message based on user credits and budget status.
- Enhanced tests for `useUsageState` to verify the correct behavior of the budget completion message under various scenarios.
- Modified the `Conversations` component to display the budget completion message appropriately based on the new logic.

* style: apply formatter fixes from pre-push hook

* refactor(rewards): update coupon section and test cases for clarity and consistency

- Renamed placeholder text and button labels in the RewardsCouponSection test to reflect updated terminology.
- Adjusted test assertions to ensure they align with the new button and placeholder names.
- Updated pricing details in billingHelpers tests to reflect new monthly and annual rates for BASIC and PRO plans.
- Enhanced the ContextGatheringStep labels for better clarity regarding email and LinkedIn processing stages.

* style(tests): format coupon code input changes for consistency

- Reformatted the coupon code input changes in the RewardsCouponSection test for improved readability and consistency.
- Ensured that the test cases maintain a uniform style for better maintainability.

* refactor: enhance accessibility and improve code structure in various components

- Added ARIA roles and attributes to the PillTabBar for better accessibility.
- Refactored the canonicalization logic for toolkit slugs into a new file for improved organization.
- Updated the IntelligenceDreamsTab and IntelligenceMemoryTab components for better readability and accessibility.
- Enhanced error handling and logging in the IntelligenceSubconsciousTab for improved debugging.
- Streamlined the ReferralRewardsSection and RewardsCommunityTab components by normalizing referral code input handling.
- Removed unused props and improved layout consistency in BillingPanel and related components.
- Updated the Skills page to handle subconscious escalation dismissals more effectively.

* feat(release): configure staging environment for deployment

- Added steps to configure the staging app environment in the release workflow.
- Set environment variables for staging, including OPENHUMAN_APP_ENV and VITE_OPENHUMAN_APP_ENV.
- Updated build and deployment steps to conditionally use staging settings based on the build target.
- Ensured proper handling of workspace paths for staging deployments.

* chore(env): add optional staging environment variable to .env.example

- Introduced OPENHUMAN_APP_ENV variable to specify the app environment as 'staging'.
- Updated .env.example to include new environment configuration for clarity.

* refactor(intelligence): streamline navigation and improve text formatting

- Simplified the navigation logic in the IntelligenceSubconsciousTab for better readability.
- Improved text formatting in the IntelligenceDreamsTab for enhanced clarity and consistency.
- Refactored import statements in toolkitMeta.tsx for better organization.
2026-04-13 18:10:07 -07:00

875 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 }}
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"
- 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