diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000000000000000000000000000000000000..f6e9a7696fb61a9d68652f0719ca4281b30991c0 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,36 @@ +# OmniVoice Docker Ignore + +# Virtual environments +.venv/ +venv/ +env/ + +# Node modules +node_modules/ +bun.lockb +.turbo/ + +# Python caches +__pycache__/ +*.py[cod] +*$py.class +.pytest_cache/ +.ruff_cache/ +.mypy_cache/ + +# Databases and user data +# We don't want to copy existing local data into the image layout! +*.db +*.sqlite +*.sqlite3 +omnivoice_data/ +frontend/dist/ + +# Git internals +.git/ +.github/ + +# IDE files +.DS_Store +.vscode/ +.idea/ diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000000000000000000000000000000000..247d3c12e6a249cfaacca9c40891242034794c09 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,8 @@ +*.png filter=lfs diff=lfs merge=lfs -text +*.wav filter=lfs diff=lfs merge=lfs -text +*.icns filter=lfs diff=lfs merge=lfs -text +*.tar.gz filter=lfs diff=lfs merge=lfs -text +*.jpg filter=lfs diff=lfs merge=lfs -text +*.jpeg filter=lfs diff=lfs merge=lfs -text +*.mp4 filter=lfs diff=lfs merge=lfs -text +*.mp3 filter=lfs diff=lfs merge=lfs -text diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000000000000000000000000000000000000..d15ddec8824fa421ac6ac8afe1defd86d33f929d --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,6 @@ +# These are supported funding model platforms + +github: [debpalash] +# ko_fi: omnivoice +# open_collective: omnivoice-studio +# custom: ["https://omnivoice.palash.dev/sponsor"] diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000000000000000000000000000000000000..d2a32601b34c8220d9b64b050c01b242f8dc585b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,38 @@ +--- +name: 🐛 Bug Report +about: Report a bug to help us improve OmniVoice Studio +title: "[Bug] " +labels: ["bug", "triage"] +assignees: [] +--- + +## Describe the bug + +A clear and concise description of what the bug is. + +## To reproduce + +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '...' +3. See error + +## Expected behavior + +What you expected to happen. + +## Screenshots / Logs + +If applicable, add screenshots or paste relevant logs from **Settings → Logs**. + +## Environment + +- **OS:** [e.g. macOS 15.2, Windows 11, Ubuntu 24.04] +- **Install method:** [Desktop app / Docker / From source] +- **Version:** [e.g. v0.2.7 — check Settings → About] +- **GPU:** [e.g. NVIDIA RTX 4090 / Apple M3 Pro / CPU only] +- **RAM:** [e.g. 16 GB] + +## Additional context + +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000000000000000000000000000000000000..b0817e5adde46e27b1a38cffb365032a28c9e14c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,23 @@ +--- +name: ✨ Feature Request +about: Suggest an idea for OmniVoice Studio +title: "[Feature] " +labels: ["enhancement"] +assignees: [] +--- + +## Is your feature request related to a problem? + +A clear description of what the problem is. Ex. "I'm always frustrated when..." + +## Describe the solution you'd like + +A clear description of what you want to happen. + +## Describe alternatives you've considered + +Any alternative solutions or features you've considered. + +## Additional context + +Add any other context, mockups, or screenshots about the feature request here. diff --git a/.github/assets/social-preview.png b/.github/assets/social-preview.png new file mode 100644 index 0000000000000000000000000000000000000000..6e8cfa59cb5c0f5f0c266f439dcd01d1e34228a0 --- /dev/null +++ b/.github/assets/social-preview.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:07904c5efe26a838f8d463ca8fdb9dbddb66bd92bd7d568629a8b570b6dc8c7c +size 1239882 diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000000000000000000000000000000000000..2fae036ee93c7689567ef65e6b31d06340ea00b6 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,49 @@ +## Summary + + + +## Changes + + + +- + +## Type + + + +- [ ] 🐛 Bug fix +- [ ] ✨ New feature +- [ ] ♻️ Refactor +- [ ] 📝 Documentation +- [ ] 🧪 Tests +- [ ] 🔧 CI / Build +- [ ] 🚀 Release prep (RC or final) + +## Testing + + + +- + +## Checklist + +- [ ] I've tested this locally +- [ ] I've updated relevant documentation (if applicable) +- [ ] No local machine paths, logs, or personal env details in this PR +- [ ] Version files are in sync (if version bump): `pyproject.toml`, `package.json`, `tauri.conf.json`, `Cargo.toml` +- [ ] If this PR changes runtime behavior, the regression fixture at `tests/fixtures/omnivoice_data/` still loads green on the `smoke-matrix` CI job (macOS + Windows + Linux) +- [ ] If this is part of a release, I've read the "Release cadence" section below and confirmed this PR targets the right RC + +## Release cadence (read once per RC) + +OmniVoice ships every minor on a **two-RC cadence**: +- `vX.Y.0-rc1` — cut from `main` once all GATE-* requirements pass; clean-VM exercise on 4 OSes (per `REL-01`) +- 48-hour soak (no new commits to release branch except fix-forward) +- `vX.Y.0` — promotion if rc1 is clean + +If your PR touches install / bootstrap / CI, it MUST land before rc1 cut, not between rc1 and the promotion. During a soak, any merge needs explicit OK from the release captain. + +## Screenshots + + diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000000000000000000000000000000000000..fa9be68d9d0cfb2808e187e4e5ed81aedecf5193 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,256 @@ +# PR-gated continuous integration — runs backend pytest + frontend node:test +# + TypeScript typecheck on every pull request and push to main. Keeps the +# heavy 4-platform Tauri bundle off this path (that's release.yml on tag +# push) so PRs turn around in a few minutes instead of ~40. + +name: CI + +on: + pull_request: + branches: [main] + push: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + +env: + # Run all JavaScript actions on Node 24 (GH deprecates Node 20 in Sep 2026). + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +jobs: + test: + name: Tests (backend + frontend) + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + + - name: Setup Python 3.11 + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + # enable-cache persists ~/.cache/uv across runs, keyed on uv.lock — + # turns `uv sync` from ~45 s cold to ~5 s warm. + - name: Install uv + uses: astral-sh/setup-uv@v3 + with: + enable-cache: true + cache-dependency-glob: "uv.lock" + + # Node 22 is needed for --experimental-strip-types so node:test can + # import .ts files directly from frontend/src/api/*. + - name: Setup Node 22 + uses: actions/setup-node@v4 + with: + node-version: '22' + + - name: Setup Bun + uses: oven-sh/setup-bun@v1 + + # apt install ffmpeg is ~30 s every run; cache the resolved .debs. + - name: System deps (ffmpeg) + uses: awalsh128/cache-apt-pkgs-action@latest + with: + packages: ffmpeg + version: 1.0 + + - name: Install Python deps + # `--all-extras` installs optional engine deps (e.g. `supertonic`) + # so their tests can exercise the real import path, not the + # "package not installed" fallback. Smoke job below stays on bare + # `uv sync` because smoke only hits /health + fixture profiles. + run: uv sync --all-extras + + - name: Run pytest + run: uv run pytest tests/ -q --tb=short + + # Docs-drift CI gate (Phase 1 INST-06). The validator extracts code + # blocks tagged `` from docs/install/*.md and asserts + # each line appears in scripts/desktop-prod.sh after normalisation. + # Its own correctness is enforced by tests/scripts/test_validate_install_docs.py + # (checker B-5) — those tests run in the previous step. + - name: Validate install docs against desktop-prod.sh + run: python scripts/validate-install-docs.py + + # `backend/tests/` stubs core.config in sys.modules to avoid the heavy + # main app import chain — that pollutes import state for other modules, + # so it runs in its own pytest session to stay isolated from tests/. + - name: Run pytest (backend/tests, isolated) + run: uv run pytest backend/tests/ -q --tb=short + + # Cache ~/.bun/install/cache keyed on bun.lock — `bun install` drops + # from ~15 s cold to near-instant on warm cache. + - name: Cache bun deps + uses: actions/cache@v4 + with: + path: ~/.bun/install/cache + key: ${{ runner.os }}-bun-${{ hashFiles('frontend/bun.lock', 'bun.lock') }} + restore-keys: | + ${{ runner.os }}-bun- + + - name: Install frontend deps + working-directory: frontend + run: bun install + + # checkJs is true in tsconfig for IDE feedback, but 947 pre-existing + # JS errors remain. Override to false in CI so only .ts files block. + # Sourced from `typecheck:ci` in frontend/package.json so the release + # workflow runs an identical command — drift broke v0.3.x release runs. + - name: Frontend typecheck + working-directory: frontend + run: bun run typecheck:ci + + - name: Run Vitest (frontend) + working-directory: frontend + run: bunx vitest run + + # Legacy node:test runner for tests/frontend/*.test.mjs + - name: Run frontend node:test (legacy) + working-directory: frontend + run: node --experimental-strip-types --no-warnings --test ../tests/frontend/*.test.mjs + + # ── Cross-platform Tauri shell check ──────────────────────────────────── + # Catches platform-specific Rust regressions on PR (cfg(target_os=...) + # gates, missing Windows/macOS deps, etc.) without spending the 15+ min + # per-platform that a full `tauri build` takes. `cargo check` is the + # lightest gate that exercises type-checking + linking for each target. + # Full bundling stays in release.yml on tag push. + tauri-cross-platform: + name: Tauri shell check (${{ matrix.label }}) + needs: test + strategy: + fail-fast: false + matrix: + include: + - os: macos-14 + label: macOS + rust_target: aarch64-apple-darwin + - os: windows-2022 + label: Windows + rust_target: x86_64-pc-windows-msvc + - os: ubuntu-22.04 + label: Linux + rust_target: x86_64-unknown-linux-gnu + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + + - name: Setup Rust (stable) + uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.rust_target }} + + # Per-target cache key so we don't conflict with the release matrix. + - name: Rust cache + uses: Swatinem/rust-cache@v2 + with: + workspaces: frontend/src-tauri -> target + key: ${{ matrix.rust_target }}-check + + - name: Setup Bun + uses: oven-sh/setup-bun@v1 + + # Linux is the only host with non-trivial Tauri build deps — + # webkit2gtk + libayatana-appindicator + xdo. Mirror release.yml. + - name: Linux system deps + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install -y \ + libwebkit2gtk-4.1-dev \ + build-essential curl wget file libxdo-dev libssl-dev \ + libayatana-appindicator3-dev librsvg2-dev \ + libasound2-dev + + - name: Cache bun deps + uses: actions/cache@v4 + with: + path: ~/.bun/install/cache + key: ${{ runner.os }}-bun-${{ hashFiles('frontend/bun.lock', 'bun.lock') }} + restore-keys: | + ${{ runner.os }}-bun- + + - name: Install frontend deps + working-directory: frontend + run: bun install + + # tauri-build's setup hook reads tauri.conf.json's `frontendDist` + # ("../dist"), which only exists after a frontend build. Without this, + # `cargo check` would fail on a fresh checkout because the embedded + # asset map can't resolve. + - name: Build frontend (for tauri.conf.json frontendDist) + working-directory: frontend + run: bun run build + + - name: Cargo check (Tauri shell) + working-directory: frontend/src-tauri + run: cargo check --target ${{ matrix.rust_target }} --message-format=short + + # ── Cross-platform Python runtime smoke (Phase 0 GATE-02) ─────────────── + # Loads the frozen tests/fixtures/omnivoice_data/ fixture and boots the + # FastAPI app in-process via TestClient on macOS/Windows/Linux. Catches + # platform-specific Python import / path bugs that the Linux-only `test` + # job above misses. Narrow scope (tests/smoke/ only) — full pytest stays + # on Linux until Phase 1's INST-01 lands setuptools for WhisperX. + smoke-matrix: + name: Smoke (${{ matrix.label }}) + needs: test + strategy: + fail-fast: false + matrix: + include: + - os: macos-14 + label: macOS + - os: windows-2022 + label: Windows + - os: ubuntu-22.04 + label: Linux + runs-on: ${{ matrix.os }} + timeout-minutes: 10 + env: + # Restricted-network resilience (RESEARCH Pitfall #6) — keeps uv from + # giving up on the first slow PyPI / python-build-standalone fetch. + UV_HTTP_TIMEOUT: "120" + UV_HTTP_RETRIES: "5" + steps: + - uses: actions/checkout@v4 + + - name: Setup Python 3.11 + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Install uv + uses: astral-sh/setup-uv@v3 + with: + enable-cache: true + cache-dependency-glob: "uv.lock" + + # ffmpeg + libsndfile are needed by soundfile / audio fixtures even + # though the silence WAV doesn't decode anything heavy — keeps test + # collection from import-erroring on optional audio modules. + - name: System deps (macOS) + if: runner.os == 'macOS' + run: brew install ffmpeg libsndfile || true + + - name: System deps (Windows) + if: runner.os == 'Windows' + shell: bash + run: | + choco install ffmpeg -y --no-progress + ffmpeg -version + + - name: System deps (Linux) + if: runner.os == 'Linux' + uses: awalsh128/cache-apt-pkgs-action@latest + with: + packages: ffmpeg libsndfile1 + version: 1.0 + + - name: Install Python deps + run: uv sync + + - name: Run smoke tests + run: uv run pytest tests/smoke/ -q --tb=short diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 0000000000000000000000000000000000000000..3aab64d510cf935cfbfd9bb61676f424b47c2805 --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,68 @@ +# Publish Docker images to GitHub Container Registry (GHCR). +# +# Triggers: +# - push of a tag matching `v*` (e.g. `v0.2.7`) → pushed as :0.2.7 + :latest +# - workflow_dispatch → pushed as :sha- (for testing) +# +# Images land at: ghcr.io/debpalash/omnivoice-studio + +name: Docker (GHCR) + +on: + push: + tags: ['v*'] + workflow_dispatch: + +permissions: + contents: read + packages: write + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +jobs: + build-and-push: + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + + # QEMU enables cross-platform builds (arm64 on x64 runner). + # Skipped for now — only building linux/amd64. + # - uses: docker/setup-qemu-action@v3 + + - 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 }} + + # Extracts semver tags from the git ref: + # v0.2.7 → 0.2.7, latest + # manual dispatch → sha-abc1234 + - name: Extract metadata (tags, labels) + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=sha,prefix=sha-,format=short + type=raw,value=latest,enable={{is_default_branch}} + + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: . + file: deploy/Dockerfile + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000000000000000000000000000000000000..ad83dba5c3c4a518f55f3ec8ee1918cf828c5158 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,516 @@ +# Desktop release pipeline — self-updating binaries for mac/linux/windows. +# +# Triggers: +# - push of a tag matching `v*` (e.g. `v0.2.0`) → full release, publishes +# artifacts + signed updater manifest (`latest.json`) to GH Releases. +# - workflow_dispatch → on-demand build, uploads artifacts as workflow +# artifacts only (no release, no updater manifest). +# +# Strategy: matrix builds per target. Each runner produces a PyInstaller +# frozen backend + Tauri bundle. `tauri-apps/tauri-action` signs the updater +# payloads with TAURI_SIGNING_PRIVATE_KEY and uploads to the GH Release for +# the tag. The built-in updater plugin polls the release's `latest.json` on +# client boot. +# +# Windows/Linux support: first-pass enabled. Expect the first few runs on +# each to surface PyInstaller/Tauri issues that never showed up locally on +# macOS — iterate on CI. + +name: Desktop Release + +on: + push: + tags: ['v*'] + workflow_dispatch: + inputs: + draft: + description: "Create as draft release (tag push only)" + required: false + default: "true" + +permissions: + contents: write # needed to attach artifacts + updater manifest to GH Release + +env: + # Run all JavaScript actions on Node 24 (GH deprecates Node 20 in Sep 2026). + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +jobs: + # Fast gating job — runs backend pytest + frontend node:test + tsc on a + # single Linux runner. The matrix build below waits on this via `needs:` + # so we don't burn 4× platform-matrix minutes on a broken commit. + test: + name: Tests (backend + frontend) + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + + - name: Setup Python 3.11 + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + # enable-cache persists ~/.cache/uv keyed on uv.lock. + - name: Install uv + uses: astral-sh/setup-uv@v3 + with: + enable-cache: true + cache-dependency-glob: "uv.lock" + + # Node 22 is needed for --experimental-strip-types so node:test can + # import .ts files directly from frontend/src/api/*. + - name: Setup Node 22 + uses: actions/setup-node@v4 + with: + node-version: '22' + + - name: Setup Bun + uses: oven-sh/setup-bun@v1 + + # Backend tests need ffmpeg (subprocess calls in fixtures). Cache the + # resolved .debs so warm runs skip the apt-get update + install. + - name: System deps (ffmpeg) + uses: awalsh128/cache-apt-pkgs-action@latest + with: + packages: ffmpeg + version: 1.0 + + - name: Install Python deps + run: uv sync + + - name: Run pytest + run: uv run pytest tests/ -q --tb=short + + - name: Cache bun deps + uses: actions/cache@v4 + with: + path: ~/.bun/install/cache + key: ${{ runner.os }}-bun-${{ hashFiles('frontend/bun.lock', 'bun.lock') }} + restore-keys: | + ${{ runner.os }}-bun- + + - name: Install frontend deps + working-directory: frontend + run: bun install + + # Single-sourced typecheck command (mirrors ci.yml). The `typecheck:ci` + # script in frontend/package.json sets `--checkJs false` so pre-existing + # JS-side errors don't block the release; only .ts/.tsx files gate. + # Drift between CI and release-time typecheck flags broke v0.3.x release + # runs — keep this command identical to ci.yml's step. + - name: Frontend typecheck + working-directory: frontend + run: bun run typecheck:ci + + # Invoke node directly (not `bun run test`) because `bun run` auto-aliases + # `node` to `bun` in script bodies, and bun doesn't support + # --experimental-strip-types. + - name: Run frontend node:test + working-directory: frontend + run: node --experimental-strip-types --no-warnings --test ../tests/frontend/*.test.mjs + + build: + needs: test + strategy: + fail-fast: false + matrix: + include: + - os: macos-14 + arch: aarch64-apple-darwin + label: "macOS Apple Silicon" + rust_target: aarch64-apple-darwin + bundles: "app,dmg,updater" + + # macOS Intel dropped: Apple shipped the last Intel Mac in 2023 and + # Rosetta 2 runs the ARM build natively. macos-13 runner backlog + # was also blocking every release tag for ~10 min. + # Windows: force MSI bundling via --bundles. NSIS fails at makensis + # because our PyInstaller payload approaches its ~2 GB stub limit. + - os: windows-2022 + arch: x86_64-pc-windows-msvc + label: "Windows x64" + rust_target: x86_64-pc-windows-msvc + bundles: "msi,updater" + + # Linux: ship .deb + .AppImage. AppImage is universal (no distro + # package-manager dep), runs on any glibc-2.31+ host. Now viable + # because the thin uv-venv installer is ~10 MB (vs the prior ~2 GB + # PyInstaller payload that exceeded linuxdeploy limits). FUSE + # unavailability on GH runners handled via APPIMAGE_EXTRACT_AND_RUN=1. + - os: ubuntu-22.04 + arch: x86_64-unknown-linux-gnu + label: "Linux x64" + rust_target: x86_64-unknown-linux-gnu + bundles: "deb,appimage,updater" + + runs-on: ${{ matrix.os }} + name: ${{ matrix.label }} + + steps: + - uses: actions/checkout@v4 + + # ── Language runtimes ────────────────────────────────────────────── + - name: Setup Rust (stable) + uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.rust_target }} + + # Cache ~/.cargo/registry + {target}/ per rust_target. Cargo dep + # compile is the long pole of the build — cold is ~5-7 min, warm + # drops to ~1-2 min. + - name: Rust cache + uses: Swatinem/rust-cache@v2 + with: + workspaces: frontend/src-tauri -> target + key: ${{ matrix.rust_target }} + + - name: Setup Bun + uses: oven-sh/setup-bun@v1 + + # ── Platform deps (Tauri host requirements only — no Python here) ─ + # The runtime Python/uv bootstrap happens on the user's machine at + # first launch, not in CI. CI only packages the source (pyproject.toml, + # uv.lock, backend/*.py) into the Tauri installer as resources. + - name: macOS system deps + if: runner.os == 'macOS' + run: | + brew install ffmpeg || true + + - name: Linux system deps + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install -y \ + libwebkit2gtk-4.1-dev \ + build-essential curl wget file libxdo-dev libssl-dev \ + libayatana-appindicator3-dev librsvg2-dev \ + libasound2-dev ffmpeg + + # ── Frontend build ───────────────────────────────────────────────── + - name: Cache bun deps + uses: actions/cache@v4 + with: + path: ~/.bun/install/cache + key: ${{ runner.os }}-bun-${{ hashFiles('frontend/bun.lock', 'bun.lock') }} + restore-keys: | + ${{ runner.os }}-bun- + + - name: Install frontend deps + working-directory: frontend + run: bun install + + # ── Tauri build + sign + publish ─────────────────────────────────── + # tauri-action handles: bundle, sign updater payload with the + # TAURI_SIGNING_PRIVATE_KEY secret, attach to release, update + # latest.json with per-platform download URLs & signatures. The + # installer ships the repo's pyproject.toml + uv.lock + backend/ + # tree as Tauri resources; lib.rs::ensure_venv_ready recreates the + # venv on first launch via `uv sync --frozen --no-dev`. + # Fetch the standalone `uv` binary for the current matrix target and + # drop it at `binaries/uv-{ext}`. tauri.conf.json + # references `binaries/uv` via `bundle.externalBin`, and tauri-bundler + # picks up the per-target file automatically. The runtime then uses + # the bundled binary instead of downloading uv on first launch. + # + # Pinned uv version mirrors the `UV_VERSION` constant in lib.rs; bump + # both together when refreshing. + - name: Bundle uv (${{ matrix.rust_target }}) + shell: bash + env: + UV_VERSION: "0.11.7" + TRIPLE: ${{ matrix.rust_target }} + run: | + set -euo pipefail + mkdir -p frontend/src-tauri/binaries + case "$TRIPLE" in + aarch64-apple-darwin|x86_64-apple-darwin|x86_64-unknown-linux-gnu) + ARCHIVE="tar.gz" + ;; + x86_64-pc-windows-msvc) + ARCHIVE="zip" + ;; + *) + echo "Unsupported target for uv bundling: $TRIPLE" + exit 1 + ;; + esac + URL="https://github.com/astral-sh/uv/releases/download/${UV_VERSION}/uv-${TRIPLE}.${ARCHIVE}" + echo "Fetching $URL" + WORK=$(mktemp -d) + if [ "$ARCHIVE" = "zip" ]; then + curl -fsSL "$URL" -o "$WORK/uv.zip" + unzip -j -o "$WORK/uv.zip" -d "$WORK" + mv "$WORK/uv.exe" "frontend/src-tauri/binaries/uv-${TRIPLE}.exe" + else + curl -fsSL "$URL" | tar -xz -C "$WORK" + mv "$WORK/uv-${TRIPLE}/uv" "frontend/src-tauri/binaries/uv-${TRIPLE}" + chmod +x "frontend/src-tauri/binaries/uv-${TRIPLE}" + fi + ls -la "frontend/src-tauri/binaries/" + + # Download static ffmpeg + ffprobe binaries and drop them into the + # Tauri sidecar directory. Sources: + # macOS: evermeet.cx — individual .zip per binary (x86_64, + # runs fine on Apple Silicon via Rosetta 2) + # Linux/Windows: BtbN/FFmpeg-Builds — single archive with both bins + # Pinned BtbN/FFmpeg-Builds version for Linux + Windows ffmpeg + # bundling. The string appears *twice* in each URL (once as the + # release tag, once inside the archive filename) — BtbN tags their + # autobuilds `autobuild-YYYY-MM-DD-HH-MM` and the inner filenames + # use the same datestamp. Driving both from one variable means a + # maintainer pin is a one-line edit: change `latest` to a specific + # autobuild tag (https://github.com/BtbN/FFmpeg-Builds/releases) to + # get reproducible installer builds. Same constant lives in + # frontend/src-tauri/src/tools.rs:FFMPEG_BTBN_VERSION — bump + # together. + - name: Bundle ffmpeg + ffprobe (${{ matrix.rust_target }}) + shell: bash + env: + TRIPLE: ${{ matrix.rust_target }} + FFMPEG_BTBN_VERSION: "latest" + run: | + set -euo pipefail + BINDIR="frontend/src-tauri/binaries" + mkdir -p "$BINDIR" + WORK=$(mktemp -d) + + case "$TRIPLE" in + aarch64-apple-darwin|x86_64-apple-darwin) + # evermeet.cx ships each binary as a separate .zip containing + # a single x86_64 Mach-O executable (runs via Rosetta on arm64). + for TOOL in ffmpeg ffprobe; do + if [ "$TOOL" = "ffmpeg" ]; then + URL="https://evermeet.cx/ffmpeg/getrelease/zip" + else + URL="https://evermeet.cx/ffmpeg/getrelease/${TOOL}/zip" + fi + echo "Fetching $TOOL from evermeet.cx" + curl -fsSL "$URL" -o "$WORK/${TOOL}.zip" + unzip -o -j "$WORK/${TOOL}.zip" -d "$WORK" + mv "$WORK/${TOOL}" "$BINDIR/${TOOL}-${TRIPLE}" + chmod +x "$BINDIR/${TOOL}-${TRIPLE}" + done + ;; + x86_64-unknown-linux-gnu) + URL="https://github.com/BtbN/FFmpeg-Builds/releases/download/${FFMPEG_BTBN_VERSION}/ffmpeg-master-${FFMPEG_BTBN_VERSION}-linux64-gpl.tar.xz" + echo "Fetching ffmpeg from BtbN (linux64) — version=${FFMPEG_BTBN_VERSION}" + curl -fsSL "$URL" -o "$WORK/ffmpeg.tar.xz" + tar -xJf "$WORK/ffmpeg.tar.xz" -C "$WORK" + # Archive extracts to ffmpeg-master-latest-linux64-gpl/bin/ + EXTRACTED=$(find "$WORK" -type d -name "bin" | head -1) + mv "$EXTRACTED/ffmpeg" "$BINDIR/ffmpeg-${TRIPLE}" + mv "$EXTRACTED/ffprobe" "$BINDIR/ffprobe-${TRIPLE}" + chmod +x "$BINDIR/ffmpeg-${TRIPLE}" "$BINDIR/ffprobe-${TRIPLE}" + ;; + x86_64-pc-windows-msvc) + URL="https://github.com/BtbN/FFmpeg-Builds/releases/download/${FFMPEG_BTBN_VERSION}/ffmpeg-master-${FFMPEG_BTBN_VERSION}-win64-gpl.zip" + echo "Fetching ffmpeg from BtbN (win64) — version=${FFMPEG_BTBN_VERSION}" + curl -fsSL "$URL" -o "$WORK/ffmpeg.zip" + unzip -o "$WORK/ffmpeg.zip" -d "$WORK" + EXTRACTED=$(find "$WORK" -type f -name "ffmpeg.exe" | head -1) + EXTRACTED_DIR=$(dirname "$EXTRACTED") + mv "$EXTRACTED_DIR/ffmpeg.exe" "$BINDIR/ffmpeg-${TRIPLE}.exe" + mv "$EXTRACTED_DIR/ffprobe.exe" "$BINDIR/ffprobe-${TRIPLE}.exe" + ;; + *) + echo "⚠ No ffmpeg bundling for target: $TRIPLE (will download at first run)" + ;; + esac + ls -la "$BINDIR/" + + + # Extract the matching CHANGELOG.md section so the release body has + # real notes instead of "see commit log". Falls back to a one-liner + # if the tag has no matching `## [X.Y.Z]` section yet — keeps the + # release publishable even when CHANGELOG hasn't been updated. + - name: Extract CHANGELOG section for tag + id: changelog + shell: bash + run: | + TAG="${GITHUB_REF_NAME#v}" + BODY="" + if [ -f CHANGELOG.md ]; then + BODY=$(awk -v tag="$TAG" ' + /^## \[/ { + if (in_section) exit + if ($0 ~ "\\[" tag "\\]") { in_section = 1; next } + } + in_section { print } + ' CHANGELOG.md | sed -e :a -e '/^\n*$/{$d;N;ba' -e '}') + fi + if [ -z "$BODY" ]; then + BODY="Auto-generated release for ${GITHUB_REF_NAME}. See [CHANGELOG.md](https://github.com/${GITHUB_REPOSITORY}/blob/main/CHANGELOG.md) and the commit log for details." + fi + { + echo 'body<> "$GITHUB_OUTPUT" + + - name: Build + release (Tauri) + uses: tauri-apps/tauri-action@v0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} + TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} + # GH runners disable FUSE, so linuxdeploy's AppImage can't mount + # itself at bundle time. This env tells linuxdeploy to extract-and-run + # instead, which works without FUSE. + APPIMAGE_EXTRACT_AND_RUN: 1 + with: + projectPath: frontend + args: --target ${{ matrix.rust_target }} --bundles ${{ matrix.bundles }} + tagName: ${{ github.ref_name }} + releaseName: "OmniVoice Studio ${{ github.ref_name }}" + releaseBody: ${{ steps.changelog.outputs.body }} + releaseDraft: ${{ inputs.draft || 'true' }} + prerelease: false + updaterJsonPreferNsis: false + includeUpdaterJson: true + + # ── Installer smoke (Phase 0 GATE-03) ───────────────────────────── + # Boot the just-built bundle on this matrix leg, poll /health, fail + # the release if it doesn't come up. Catches bundle-only regressions + # (PyInstaller missing-module, Tauri sidecar path mismatch, etc.) + # that the in-process smoke matrix on ci.yml cannot see. + - name: Installer smoke (macOS) + if: runner.os == 'macOS' + timeout-minutes: 5 + shell: bash + run: | + set -euo pipefail + DMG=$(find frontend/src-tauri/target/${{ matrix.rust_target }}/release/bundle/dmg -name "*.dmg" | head -1) + echo "Smoke-testing DMG: $DMG" + MOUNT=$(hdiutil attach -nobrowse -readonly "$DMG" | tail -1 | awk '{print $3}') + APP=$(find "$MOUNT" -maxdepth 2 -name "*.app" | head -1) + # RESEARCH Pitfall #5: do NOT launch the Tauri WebView shell on a headless runner — it hangs. + # The `--health-check` flag lives in backend/main.py (Python), not the Rust WebView main. + # Strategy: invoke the bundled Python backend directly, bypassing Tauri's window code. + BACKEND=$(find "$APP/Contents" -type f \( -name 'backend' -o -name 'backend.app' -o -name 'main.py' \) -perm +111 2>/dev/null | head -1) + if [ -z "$BACKEND" ]; then + # Fallback: try the PyInstaller sidecar location Tauri uses. + BACKEND=$(find "$APP/Contents/Resources" -type f \( -name 'backend*' -o -name 'omnivoice*' \) -perm +111 2>/dev/null | head -1) + fi + if [ -z "$BACKEND" ]; then + echo "FAIL — could not locate bundled backend binary in $APP. Contents:" + find "$APP/Contents" -type f -perm +111 | head -30 + hdiutil detach "$MOUNT" || true + exit 1 + fi + echo "Launching bundled backend: $BACKEND --health-check" + "$BACKEND" --health-check + EXIT=$? + hdiutil detach "$MOUNT" || true + exit $EXIT + + - name: Installer smoke (Windows) + if: runner.os == 'Windows' + timeout-minutes: 5 + shell: bash + run: | + set -euo pipefail + MSI=$(find frontend/src-tauri/target/${{ matrix.rust_target }}/release/bundle/msi -name "*.msi" | head -1) + echo "Smoke-testing MSI: $MSI" + # /quiet = no UI, /norestart = don't reboot the runner if a dep asks + msiexec.exe //i "$(cygpath -w "$MSI")" //quiet //norestart + # Tauri installs to "Program Files\OmniVoice Studio\..." by default. Backend is a sidecar binary + # (RESEARCH Pitfall #5) — not the Tauri WebView .exe — so locate by name pattern. + BACKEND=$(find "/c/Program Files/OmniVoice Studio" -type f \( -name 'backend.exe' -o -name 'omnivoice-backend.exe' -o -name 'main.exe' \) 2>/dev/null | head -1) + if [ -z "$BACKEND" ]; then + echo "FAIL — bundled backend .exe not found under C:/Program Files/OmniVoice Studio. Contents:" + find "/c/Program Files/OmniVoice Studio" -type f -name '*.exe' | head -20 + exit 1 + fi + echo "Launching bundled backend: $BACKEND --health-check" + "$BACKEND" --health-check & + BACKEND_PID=$! + # Wait for completion (--health-check is a short-lived, exits-after-200 invocation) + wait $BACKEND_PID + EXIT=$? + # RESEARCH Pitfall #2: cleanup orphaned PyInstaller child processes on port 3900. + # Safe on GH-hosted ephemeral runners; REQUIRED if/when we move to self-hosted Windows. + taskkill //F //T //PID $BACKEND_PID 2>/dev/null || echo "backend process already exited cleanly" + exit $EXIT + + - name: Installer smoke (Linux) + if: runner.os == 'Linux' + timeout-minutes: 5 + shell: bash + run: | + set -euo pipefail + # Use the AppImage — single-file, no installer needed. + APPIMAGE=$(find frontend/src-tauri/target/${{ matrix.rust_target }}/release/bundle/appimage -name "*.AppImage" | head -1) + echo "Smoke-testing AppImage: $APPIMAGE" + chmod +x "$APPIMAGE" + # GH runners have no FUSE — extract before running (mirrors APPIMAGE_EXTRACT_AND_RUN=1 used at build time). + EXTRACT_DIR="$(mktemp -d)" + cd "$EXTRACT_DIR" + "$APPIMAGE" --appimage-extract >/dev/null + # Tauri's AppRun lives at squashfs-root/AppRun; the actual binary is in squashfs-root/usr/bin/ + BIN=$(find squashfs-root -type f -name "OmniVoice Studio" -o -name "omnivoice-studio" 2>/dev/null | head -1) + if [ -z "$BIN" ]; then + BIN="$EXTRACT_DIR/squashfs-root/AppRun" + fi + echo "Launching under xvfb-run: $BIN --health-check" + sudo apt-get install -y xvfb >/dev/null 2>&1 || true + xvfb-run -a "$BIN" --health-check + + # ── Compute SHA-256 checksums (Phase 0 GATE-05) ─────────────────── + # Native OS tools: shasum -a 256 (POSIX) / Get-FileHash (Windows). + # Writes SHA256SUMS-