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