Files
openhuman/.github/workflows/deploy.yml
T

684 lines
29 KiB
YAML

---
# Deployment workflow for OpenHuman
# Handles deployment of both the Rust core (openhuman-core CLI) and the React + Tauri desktop app.
#
# This workflow:
# - Validates the environment and build prerequisites
# - Runs pre-deployment checks (TypeScript compilation, linting)
# - Resolves platform filter matrix so build jobs only run for selected platforms
# - Compiles the Rust core + CLI binaries for all requested platforms
# - Builds the React frontend + Tauri desktop bundles (macOS, Linux, Windows)
# - Stages artifacts for distribution
# - Signs and notarizes macOS bundles (production only)
# - Publishes release assets and Docker images
#
# Triggers:
# - Manual dispatch for on-demand deployments (specify environment: staging/production)
#
# ─────────────────────────────────────────────────────────────────────────────
# FIX LOG (vs. previous revision)
# ─────────────────────────────────────────────────────────────────────────────
# [F-1] CRITICAL — matrix context unavailable in job-level `if`.
# `matrix.*` is only resolved inside step-level expressions; a job-level
# `if: contains(..., matrix.platform_filter)` always evaluates against an
# empty string and silently skips every matrix leg.
# Fix: added a `resolve-matrix` job that converts the comma-separated
# input into a JSON include-list; `build-rust-core` and `build-desktop`
# now consume `needs.resolve-matrix.outputs.*` which IS available at
# job-level. Platform filtering is therefore correct and deterministic.
#
# [F-2] `pre-checks` skipped when `skip_tests=true` stalled all build jobs.
# Downstream jobs listed `pre-checks` in `needs`, so when it was
# conditionally skipped GitHub marked dependents as "not run".
# Fix: build jobs now use
# `if: needs.pre-checks.result == 'success' ||
# needs.pre-checks.result == 'skipped'`
# so they proceed whether tests ran or were intentionally bypassed.
#
# [F-3] `publish-release` condition used `&&` but `build-docker` can be
# 'skipped' when docker is excluded from `deploy_platforms`.
# Fix: condition now explicitly allows 'skipped' for build-docker:
# `build-docker.result != 'failure'`
# Analogous guard added for build-rust-core and build-desktop.
#
# [F-4] Docker `type=semver` tag pattern requires a pushed git tag; on a
# `workflow_dispatch` trigger it produces an empty/no-op tag.
# Fix: replaced with explicit `type=raw,value=${{ needs.validate.outputs.version }}`
# and kept the SHA and branch tags which work on any trigger.
#
# [F-5] `actions/checkout@v5` does not exist (latest stable is v4).
# `softprops/action-gh-release@v3.0.0` does not exist (latest is v2).
# Fix: pinned all actions to their current latest major version.
#
# [F-6] Rust toolchain version was duplicated — `env.RUST_VERSION` defined at
# the top level but `dtolnay/rust-toolchain@1.93.0` hard-coded the same
# version inline, creating a drift risk.
# Fix: `dtolnay/rust-toolchain@stable` is now called with
# `toolchain: ${{ env.RUST_VERSION }}` so there is a single source of truth.
#
# [F-7] No `timeout-minutes` on long-running build jobs; a hung runner could
# block the entire workflow indefinitely.
# Fix: added sensible timeouts (60 min for Rust core, 90 min for desktop).
# ─────────────────────────────────────────────────────────────────────────────
name: Deploy
on:
workflow_dispatch:
inputs:
environment:
description: Deployment environment (staging | production)
required: true
type: choice
default: staging
options:
- staging
- production
skip_tests:
description: Skip pre-deployment tests (use with caution)
required: false
type: boolean
default: false
deploy_platforms:
description: |
Platforms to deploy (comma-separated).
Options: macos-arm64, macos-x64, linux-x64, linux-arm64, windows-x64, docker
required: false
type: string
default: "macos-arm64,macos-x64,linux-x64,linux-arm64,windows-x64,docker"
permissions:
contents: write
packages: write
concurrency:
group: deploy-${{ github.ref }}-${{ inputs.environment || 'staging' }}
cancel-in-progress: false
env:
REGISTRY: ghcr.io
IMAGE_NAME: tinyhumansai/openhuman-core
NODE_VERSION: 24.x
RUST_VERSION: 1.83.0 # single source of truth — referenced in all toolchain steps
jobs:
# =========================================================================
# Phase 0: Validate deployment context
# =========================================================================
validate:
name: Validate deployment context
runs-on: ubuntu-latest
environment: ${{ inputs.environment || 'staging' }}
outputs:
platforms: ${{ steps.platforms.outputs.list }}
ref: ${{ steps.context.outputs.ref }}
version: ${{ steps.context.outputs.version }}
short_sha: ${{ steps.context.outputs.short_sha }}
release_date: ${{ steps.context.outputs.release_date }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Resolve deployment context
id: context
shell: bash
run: |
set -euo pipefail
VERSION=$(node -e 'console.log(JSON.parse(require("fs").readFileSync("app/package.json")).version)')
SHORT_SHA="${GITHUB_SHA:0:12}"
RELEASE_DATE="$(date -u +'%Y-%m-%dT%H:%M:%SZ')"
echo "ref=$GITHUB_REF_NAME" >> "$GITHUB_OUTPUT"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "short_sha=$SHORT_SHA" >> "$GITHUB_OUTPUT"
echo "release_date=$RELEASE_DATE" >> "$GITHUB_OUTPUT"
echo "[deploy] environment=${{ inputs.environment }}"
echo "[deploy] ref=${GITHUB_REF_NAME}"
echo "[deploy] version=${VERSION}"
echo "[deploy] short_sha=${SHORT_SHA}"
- name: Parse platform filters
id: platforms
shell: bash
env:
DEPLOY_PLATFORMS: ${{ inputs.deploy_platforms }}
run: |
PLATFORMS="${DEPLOY_PLATFORMS:-macos-arm64,macos-x64,linux-x64,linux-arm64,windows-x64,docker}"
echo "list=${PLATFORMS}" >> "$GITHUB_OUTPUT"
echo "[deploy] platforms=${PLATFORMS}"
- name: Validate prerequisites
shell: bash
env:
ENVIRONMENT: ${{ inputs.environment }}
run: |
set -euo pipefail
case "$ENVIRONMENT" in
staging|production)
echo "[deploy] Valid environment: $ENVIRONMENT"
;;
*)
echo "[deploy] ERROR: Invalid environment '$ENVIRONMENT'"
exit 1
;;
esac
# =========================================================================
# Phase 0b: Resolve dynamic matrix — [F-1]
#
# matrix.* context is NOT available in job-level `if` expressions; only
# step-level expressions can read it. This job converts the
# `deploy_platforms` string into explicit JSON include-arrays so that
# build-rust-core and build-desktop can use job-level `if` guards safely.
# =========================================================================
resolve-matrix:
name: Resolve build matrix
runs-on: ubuntu-latest
needs: validate
outputs:
rust_matrix: ${{ steps.resolve.outputs.rust_matrix }}
desktop_matrix: ${{ steps.resolve.outputs.desktop_matrix }}
include_docker: ${{ steps.resolve.outputs.include_docker }}
steps:
- name: Compute include arrays
id: resolve
shell: bash
env:
PLATFORMS: ${{ needs.validate.outputs.platforms }}
run: |
set -euo pipefail
# ── Rust core matrix ──────────────────────────────────────────────
RUST_INCLUDES="[]"
ALL_RUST='[
{"target":"x86_64-unknown-linux-gnu", "runner":"ubuntu-24.04", "platform_filter":"linux-x64"},
{"target":"aarch64-unknown-linux-gnu", "runner":"ubuntu-24.04-arm", "platform_filter":"linux-arm64"},
{"target":"x86_64-pc-windows-msvc", "runner":"windows-latest", "platform_filter":"windows-x64"},
{"target":"aarch64-apple-darwin", "runner":"macos-latest", "platform_filter":"macos-arm64"},
{"target":"x86_64-apple-darwin", "runner":"macos-latest", "platform_filter":"macos-x64"}
]'
RUST_INCLUDES=$(echo "$ALL_RUST" | python3 -c "
import json, sys, os
platforms = os.environ['PLATFORMS'].split(',')
items = json.load(sys.stdin)
print(json.dumps([i for i in items if i['platform_filter'] in platforms]))
")
# ── Desktop (Tauri) matrix ─────────────────────────────────────────
ALL_DESKTOP='[
{"platform":"macos-latest", "args":"--target aarch64-apple-darwin", "target":"aarch64-apple-darwin", "artifact_suffix":"aarch64-apple-darwin", "platform_filter":"macos-arm64"},
{"platform":"macos-latest", "args":"--target x86_64-apple-darwin", "target":"x86_64-apple-darwin", "artifact_suffix":"x86_64-apple-darwin", "platform_filter":"macos-x64"},
{"platform":"ubuntu-24.04", "args":"--target x86_64-unknown-linux-gnu --bundles deb appimage","target":"x86_64-unknown-linux-gnu", "artifact_suffix":"ubuntu", "platform_filter":"linux-x64"},
{"platform":"ubuntu-24.04-arm","args":"--target aarch64-unknown-linux-gnu --bundles deb appimage","target":"aarch64-unknown-linux-gnu", "artifact_suffix":"ubuntu-arm64", "platform_filter":"linux-arm64"},
{"platform":"windows-latest", "args":"--target x86_64-pc-windows-msvc", "target":"x86_64-pc-windows-msvc", "artifact_suffix":"windows", "platform_filter":"windows-x64"}
]'
DESKTOP_INCLUDES=$(echo "$ALL_DESKTOP" | python3 -c "
import json, sys, os
platforms = os.environ['PLATFORMS'].split(',')
items = json.load(sys.stdin)
print(json.dumps([i for i in items if i['platform_filter'] in platforms]))
")
# ── Docker flag ────────────────────────────────────────────────────
if echo "$PLATFORMS" | grep -q "docker"; then
INCLUDE_DOCKER="true"
else
INCLUDE_DOCKER="false"
fi
echo "rust_matrix={\"include\":${RUST_INCLUDES}}" >> "$GITHUB_OUTPUT"
echo "desktop_matrix={\"include\":${DESKTOP_INCLUDES}}" >> "$GITHUB_OUTPUT"
echo "include_docker=${INCLUDE_DOCKER}" >> "$GITHUB_OUTPUT"
echo "[matrix] rust_includes=${RUST_INCLUDES}"
echo "[matrix] desktop_includes=${DESKTOP_INCLUDES}"
echo "[matrix] include_docker=${INCLUDE_DOCKER}"
# =========================================================================
# Phase 1: Pre-deployment checks (type check, lint)
# =========================================================================
pre-checks:
name: Pre-deployment checks
runs-on: ubuntu-latest
needs: validate
if: ${{ !inputs.skip_tests }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
- name: Setup pnpm
uses: pnpm/action-setup@v4
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Type check (TypeScript)
run: pnpm compile
- name: Lint
run: pnpm lint
# =========================================================================
# Phase 2: Build Rust core binaries
# =========================================================================
build-rust-core:
name: Build Rust core (${{ matrix.target }})
runs-on: ${{ matrix.runner }}
needs: [validate, resolve-matrix, pre-checks]
timeout-minutes: 60 # [F-7] prevent hung runners blocking the pipeline
# [F-2] proceed when pre-checks succeeded OR was intentionally skipped
if: |
fromJSON(needs.resolve-matrix.outputs.rust_matrix).include[0] != null &&
(needs.pre-checks.result == 'success' || needs.pre-checks.result == 'skipped')
strategy:
fail-fast: false
matrix: ${{ fromJSON(needs.resolve-matrix.outputs.rust_matrix) }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Install Rust # [F-6] single source of truth via env.RUST_VERSION
uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ env.RUST_VERSION }}
targets: ${{ matrix.target }}
- name: Cache Cargo registry
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
key: ${{ runner.os }}-cargo-${{ matrix.target }}-${{ hashFiles('Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-${{ matrix.target }}-
- name: Build openhuman-core binary
shell: bash
env:
TARGET: ${{ matrix.target }}
OPENHUMAN_BUILD_SHA: ${{ needs.validate.outputs.short_sha }}
run: |
set -euo pipefail
cargo build \
--manifest-path Cargo.toml \
--release \
--target "$TARGET" \
--bin openhuman-core
echo "[build] openhuman-core compiled for $TARGET"
- name: Stage core binary
shell: bash
env:
TARGET: ${{ matrix.target }}
run: |
set -euo pipefail
mkdir -p artifacts/core
if [ "${{ runner.os }}" = "Windows" ]; then
BIN_NAME="openhuman-core.exe"
else
BIN_NAME="openhuman-core"
fi
cp "target/$TARGET/release/$BIN_NAME" "artifacts/core/$BIN_NAME"
ls -lh "artifacts/core/$BIN_NAME"
- name: Upload core binary
uses: actions/upload-artifact@v4
with:
name: core-${{ matrix.target }}
path: artifacts/core/
retention-days: 7
# =========================================================================
# Phase 3: Build desktop bundles (Tauri + React)
# =========================================================================
build-desktop:
name: Build desktop (${{ matrix.artifact_suffix }})
runs-on: ${{ matrix.platform }}
needs: [validate, resolve-matrix, pre-checks]
timeout-minutes: 90 # [F-7]
environment: ${{ inputs.environment || 'staging' }}
# [F-2] same skip-guard as build-rust-core
if: |
fromJSON(needs.resolve-matrix.outputs.desktop_matrix).include[0] != null &&
(needs.pre-checks.result == 'success' || needs.pre-checks.result == 'skipped')
strategy:
fail-fast: false
matrix: ${{ fromJSON(needs.resolve-matrix.outputs.desktop_matrix) }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
OPENHUMAN_TELEGRAM_BOT_USERNAME: ${{ vars.OPENHUMAN_TELEGRAM_BOT_USERNAME }}
VITE_TELEGRAM_BOT_USERNAME: ${{ vars.OPENHUMAN_TELEGRAM_BOT_USERNAME }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 1
submodules: recursive
- name: Set Xcode version (macOS)
if: matrix.platform == 'macos-latest'
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: latest-stable
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
cache: true
- name: Install Rust # [F-6]
uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ env.RUST_VERSION }}
targets: ${{ (matrix.platform == 'macos-latest' && 'aarch64-apple-darwin,x86_64-apple-darwin') || matrix.target }}
- name: Install Tauri dependencies (Ubuntu)
if: startsWith(matrix.platform, 'ubuntu-')
run: |
sudo apt-get update
sudo apt-get install -y \
libgtk-3-dev libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev \
patchelf cmake libasound2-dev libxdo-dev libxtst-dev libx11-dev libxi-dev \
libevdev-dev libssl-dev libclang-dev desktop-file-utils \
libnss3 libnspr4 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm2 \
libxkbcommon0 libxcomposite1 libxdamage1 libxfixes3 libxrandr2 \
libgbm1 libpango-1.0-0 libcairo2 libatspi2.0-0 libxshmfence1 libu2f-udev
- name: Cache Cargo artifacts
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
app/src-tauri/target
key: ${{ runner.os }}-cargo-tauri-${{ matrix.target }}-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-tauri-${{ matrix.target }}-
- name: Cache CEF distribution
uses: actions/cache@v4
with:
path: |
~/Library/Caches/tauri-cef
~/.cache/tauri-cef
~/AppData/Local/tauri-cef
key: cef-${{ matrix.target }}-${{ hashFiles('app/src-tauri/Cargo.toml') }}
restore-keys: |
cef-${{ matrix.target }}-
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build desktop application
shell: bash
working-directory: app
env:
BASE_URL: ${{ inputs.environment == 'production' && 'https://api.tinyhumans.ai/' || 'https://staging-api.tinyhumans.ai/' }}
OPENHUMAN_APP_ENV: ${{ inputs.environment || 'staging' }}
VITE_OPENHUMAN_APP_ENV: ${{ inputs.environment || 'staging' }}
VITE_BUILD_SHA: ${{ needs.validate.outputs.short_sha }}
SENTRY_RELEASE: openhuman@${{ needs.validate.outputs.version }}+${{ needs.validate.outputs.short_sha }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_URL: ${{ vars.SENTRY_URL }}
SENTRY_ORG: ${{ vars.SENTRY_ORG }}
SENTRY_PROJECT: ${{ vars.SENTRY_PROJECT_REACT }}
VITE_SENTRY_DSN: ${{ vars.OPENHUMAN_REACT_SENTRY_DSN }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
WITH_UPDATER: "true"
MACOSX_DEPLOYMENT_TARGET: ${{ matrix.platform == 'macos-latest' && '10.15' || '' }}
run: |
set -euo pipefail
pnpm tauri:ensure
if [ "${{ runner.os }}" = "Linux" ]; then
NODE_OPTIONS="--max-old-space-size=8192" cargo tauri build \
--no-bundle ${{ matrix.args }}
CEF_LIB_DIR="$(find "$HOME/.cache/tauri-cef" -name libcef.so -printf '%h\n' 2>/dev/null | head -1)"
if [ -z "$CEF_LIB_DIR" ]; then
echo "::error::libcef.so not found; cannot satisfy lib4bin ldd resolution."
exit 1
fi
export LD_LIBRARY_PATH="$CEF_LIB_DIR${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
fi
NODE_OPTIONS="--max-old-space-size=8192" cargo tauri build ${{ matrix.args }}
echo "[build] Desktop application compiled for ${{ matrix.target }}"
- name: Sign and notarize macOS bundle
if: matrix.platform == 'macos-latest' && inputs.environment == 'production'
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: |
set -euo pipefail
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 "::error::Missing macOS signing secret: $var"
exit 1
fi
done
CERT_PATH="${RUNNER_TEMP}/apple_cert.p12"
echo "$APPLE_CERTIFICATE_BASE64" | base64 --decode > "$CERT_PATH"
KEYCHAIN_PATH="${RUNNER_TEMP}/build.keychain"
security create-keychain -p "$APPLE_CERTIFICATE_PASSWORD" "$KEYCHAIN_PATH"
security default-keychain -s "$KEYCHAIN_PATH"
security unlock-keychain -p "$APPLE_CERTIFICATE_PASSWORD" "$KEYCHAIN_PATH"
security import "$CERT_PATH" -P "$APPLE_CERTIFICATE_PASSWORD" -A -t cert -f pkcs12 -k "$KEYCHAIN_PATH"
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$APPLE_CERTIFICATE_PASSWORD" "$KEYCHAIN_PATH"
APP_BUNDLE="$(find app/src-tauri/target/${{ matrix.target }}/release/bundle -name '*.app' -type d | head -1)"
if [ -z "$APP_BUNDLE" ]; then
echo "::warning::No .app bundle found to notarize"
else
echo "[deploy] Signing $APP_BUNDLE"
codesign -s "$APPLE_SIGNING_IDENTITY" --deep --force --verify --verbose "$APP_BUNDLE"
DMG_FILE="app/src-tauri/target/${{ matrix.target }}/release/bundle/dmg/openhuman.dmg"
if [ -f "$DMG_FILE" ]; then
echo "[deploy] Notarizing $DMG_FILE"
xcrun notarytool submit "$DMG_FILE" \
--apple-id "$APPLE_ID" \
--password "$APPLE_PASSWORD" \
--team-id "$APPLE_TEAM_ID" \
--wait || echo "::warning::Notarization submitted (may require additional time)"
fi
fi
security delete-keychain "$KEYCHAIN_PATH"
- name: Stage desktop artifacts
shell: bash
env:
TARGET: ${{ matrix.target }}
run: |
set -euo pipefail
mkdir -p artifacts/desktop
if [ -d "app/src-tauri/target/$TARGET/release/bundle" ]; then
cp -r app/src-tauri/target/$TARGET/release/bundle/* artifacts/desktop/ 2>/dev/null || true
fi
find artifacts/desktop -type f -exec ls -lh {} \;
- name: Upload desktop artifacts
uses: actions/upload-artifact@v4
with:
name: desktop-${{ matrix.artifact_suffix }}
path: artifacts/desktop/
retention-days: 7
# =========================================================================
# Phase 4: Build and push Docker image
# =========================================================================
build-docker:
name: Build and push Docker image
runs-on: ubuntu-latest
needs: [validate, resolve-matrix, pre-checks]
timeout-minutes: 45 # [F-7]
environment: ${{ inputs.environment || 'staging' }}
# [F-2] skip guard; also gated by the docker flag from resolve-matrix
if: |
needs.resolve-matrix.outputs.include_docker == 'true' &&
(needs.pre-checks.result == 'success' || needs.pre-checks.result == 'skipped')
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ github.ref }}
fetch-depth: 1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata # [F-4] replaced semver tag with explicit version raw tag
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=raw,value=${{ needs.validate.outputs.version }}
type=raw,value=${{ needs.validate.outputs.version }}-${{ needs.validate.outputs.short_sha }}
type=raw,value=${{ inputs.environment || 'staging' }}-${{ needs.validate.outputs.short_sha }}
type=ref,event=branch
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
# =========================================================================
# Phase 5: Publish release assets
# =========================================================================
publish-release:
name: Publish release assets
runs-on: ubuntu-latest
needs: [validate, build-rust-core, build-desktop, build-docker]
# [F-3] build-docker / build-rust-core / build-desktop may be 'skipped'
# when those platform groups were excluded from deploy_platforms — treat
# 'skipped' as acceptable, only block on an explicit 'failure'.
if: |
always() &&
needs.build-rust-core.result != 'failure' &&
needs.build-desktop.result != 'failure' &&
needs.build-docker.result != 'failure'
environment: ${{ inputs.environment || 'staging' }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: dist/
- name: Create checksum manifest
shell: bash
run: |
set -euo pipefail
cd dist/
find . -type f \( -name "*.exe" -o -name "*.dmg" -o -name "*.deb" -o -name "*.AppImage" -o -name "*.tar.gz" -o -name "openhuman-core*" \) | while read f; do
sha256sum "$f" >> CHECKSUMS.txt
done
echo "::notice::Checksums generated"
cat CHECKSUMS.txt || echo "(no artifacts found)"
- name: Create GitHub Release # [F-5] correct release action version
if: inputs.environment == 'production'
uses: softprops/action-gh-release@v2
with:
tag_name: v${{ needs.validate.outputs.version }}
name: OpenHuman v${{ needs.validate.outputs.version }}
body: |
# OpenHuman v${{ needs.validate.outputs.version }}
**Build Info:**
- Commit: ${{ github.sha }}
- Environment: ${{ inputs.environment }}
- Build Date: ${{ needs.validate.outputs.release_date }}
**Artifacts:**
- Desktop applications (macOS, Linux, Windows)
- CLI binaries (openhuman-core)
- Docker image: `${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.validate.outputs.version }}`
**Checksums:** See attached CHECKSUMS.txt file.
draft: false
prerelease: false
files: dist/CHECKSUMS.txt
- name: Summary
shell: bash
run: |
echo "## Release Summary" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
echo "| -------- | ----- |" >> $GITHUB_STEP_SUMMARY
echo "| Environment | ${{ inputs.environment }} |" >> $GITHUB_STEP_SUMMARY
echo "| Version | ${{ needs.validate.outputs.version }} |" >> $GITHUB_STEP_SUMMARY
echo "| Build SHA | ${{ needs.validate.outputs.short_sha }} |" >> $GITHUB_STEP_SUMMARY
echo "| Build Date | ${{ needs.validate.outputs.release_date }} |" >> $GITHUB_STEP_SUMMARY
echo "| Platforms | ${{ needs.validate.outputs.platforms }} |" >> $GITHUB_STEP_SUMMARY
# =========================================================================
# Phase 6: Verification
# =========================================================================
verify:
name: Verify deployment
runs-on: ubuntu-latest
needs: [validate, publish-release]
if: always()
steps:
- name: Check status
shell: bash
run: |
echo "[deploy] Deployment complete"
echo " Version: ${{ needs.validate.outputs.version }}"
echo " Platforms: ${{ needs.validate.outputs.platforms }}"
echo " Release: ${{ needs.publish-release.result }}"