* fix(linux): AppImage blank window on Mesa 26.1+ hosts (#1258, #1244) The AppImage bundles an Ubuntu-built WebKitGTK but ships no libEGL, so that bundled WebKit runs against the HOST's Mesa. On Mesa >= 26.1 it calls eglGetPlatformDisplay() in a way the newer driver rejects and the app dies before it paints: Could not create default EGL display: EGL_BAD_PARAMETER. Aborting... No environment variable helps, because the failure is in EGL display creation — before WebKit consults any rendering-path flag. #1258 confirmed WEBKIT_DISABLE_DMABUF_RENDERER, WEBKIT_DMABUF_RENDERER_FORCE_SHM, WEBKIT_SKIA_ENABLE_CPU_RENDERING, EGL_PLATFORM=surfaceless and MESA_LOADER_DRIVER_OVERRIDE=swrast all fail identically. Chasing the build runner's WebKit (#961 bumped 22.04 -> 24.04) cannot fix this class: what we bundle is frozen and host Mesa keeps moving. So when the host has a WebKitGTK at least as new as ours, let it win — the bundle still fills every gap, and a host without WebKitGTK is untouched. That is exactly why building from source works on the hardware where the AppImage does not. The compositing workaround is re-decided against whichever library ends up running, and AppRun.test.sh — which had never been wired into CI — now runs there, so this logic stops being a regression test nothing executes. * fix(review): the ordering change was a no-op; name the host libdir explicitly CodeRabbit Major — correct, and it made the whole fix inert. LD_LIBRARY_PATH is searched AHEAD of the linker's default paths no matter where in that variable a directory sits, so on a normal launch (empty LD_LIBRARY_PATH) the bundle remained the only explicit search directory and still won. Merely appending it changed nothing. The host's WebKit libdir is now named explicitly, ahead of ours. The new tests fail 3/3 against the previous version. Greptile P1 — a host with the runtime but no -dev package has no .pc file, so pkg-config can't answer and the check rejected a perfectly good system WebKit. The libdir probe now falls back to ldconfig, and OMNIVOICE_PREFER_SYSTEM_WEBKIT gives those users an explicit opt-in (=0 opts out) rather than gambling on an unverified version, which would risk the #961 regression. CodeRabbit — my changelog script had also inserted the CI entry into the published 0.4.0 section. Removed; it belongs only under Unreleased. CodeRabbit — the docs' source-build fallback used 'cd frontend', not the repo-root flow the rest of the page documents. Fixed.
362 lines
15 KiB
YAML
362 lines
15 KiB
YAML
# 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
|
|
|
|
# HF_HUB_OFFLINE=1 is a recurrence guard, not an optimization: a test
|
|
# that reaches huggingface.co fails fast and loud instead of silently
|
|
# downloading model weights mid-suite (the preload_model() Hub-probe
|
|
# bug pulled the full 2.3 GB k2-fsa/OmniVoice checkpoint into every
|
|
# networked empty-cache run before it was caught). All legitimate HF
|
|
# interactions in tests are stubbed; anything that trips this is a
|
|
# test-isolation bug.
|
|
- name: Run pytest
|
|
run: uv run pytest tests/ -q --tb=short
|
|
env:
|
|
HF_HUB_OFFLINE: "1"
|
|
|
|
# Docs-drift CI gate (Phase 1 INST-06). The validator extracts code
|
|
# blocks tagged `<!-- validate -->` 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
|
|
|
|
# The AppImage launcher decides which WebKitGTK actually runs — the wrong
|
|
# answer is a permanently blank window on Linux (#56, #961, #1258), and
|
|
# the only place that logic is exercised is this shell harness. It had
|
|
# never been wired into CI, so its cases were a regression test nothing
|
|
# ran. Cheap (pure bash, stubs pkg-config) and it gates the class.
|
|
- name: AppImage launcher (AppRun) unit tests
|
|
run: bash frontend/src-tauri/appimage/AppRun.test.sh
|
|
|
|
# `backend/tests/` mounts routers on bare FastAPI apps (no heavy main
|
|
# import chain) with a hermetic data dir from its conftest.py. It no
|
|
# longer stubs sys.modules, so mixed sessions with tests/ are safe;
|
|
# the separate session is kept for cheaper, clearer CI output.
|
|
- name: Run pytest (backend/tests, isolated)
|
|
run: uv run pytest backend/tests/ -q --tb=short
|
|
env:
|
|
HF_HUB_OFFLINE: "1" # same no-silent-downloads guard as tests/
|
|
|
|
# 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
|
|
# --frozen-lockfile so a frontend/package.json change that forgets to
|
|
# regenerate the root bun.lock fails HERE (fast) instead of only in the
|
|
# Docker build (deploy/Dockerfile), which is what reddened main on #485.
|
|
run: bun install --frozen-lockfile
|
|
|
|
# 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
|
|
|
|
# oxlint gate — fast Rust linter, blocks on errors so lint debt can't
|
|
# re-accumulate (warnings, incl. the react-compiler advisories in
|
|
# `lint:hooks`, are non-blocking). See frontend/.oxlintrc.json.
|
|
- name: Frontend lint (oxlint)
|
|
working-directory: frontend
|
|
run: bun run lint
|
|
|
|
# oxfmt format gate — JS/TS/JSX only (CSS/JSON/Tauri excluded; see
|
|
# frontend/.oxfmtrc.json). `bun run format` fixes locally.
|
|
- name: Frontend format check (oxfmt)
|
|
working-directory: frontend
|
|
run: bun run format:check
|
|
|
|
# `bun run test` (frontend/package.json), not `bunx vitest` — bunx
|
|
# resolves by npm package name and can miss workspace-hoisted bins,
|
|
# then falls back to fetching from npm (#962 class).
|
|
- name: Run Vitest (frontend)
|
|
working-directory: frontend
|
|
run: bun run test
|
|
|
|
# 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
|
|
|
|
# Production-bundle blank-screen gate. Everything above runs UN-minified
|
|
# (dev server + Vitest/jsdom), so a crash that exists ONLY in the minified
|
|
# release bundle — a TDZ reorder that throws before React mounts — passes
|
|
# every check and ships a black screen. That is how v0.3.22 went out (#1178),
|
|
# and it recurred pre-0.3.23. This builds the real dist/ and asserts the app
|
|
# actually mounts into #root. See frontend/e2e-prod/prod-bundle-smoke.spec.ts.
|
|
# (The in-app root <ErrorBoundary> in main-app.jsx catches such throws at
|
|
# runtime; this gate stops them reaching a release in the first place.)
|
|
- name: Install Playwright chromium
|
|
working-directory: frontend
|
|
run: bunx playwright install --with-deps chromium
|
|
- name: Production-bundle smoke — no blank screen
|
|
working-directory: frontend
|
|
run: bun run test:prod-bundle
|
|
|
|
# ── 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,
|
|
# and `cargo test --lib` runs the shell's unit tests natively on each OS.
|
|
# 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-24.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
|
|
# --frozen-lockfile so a frontend/package.json change that forgets to
|
|
# regenerate the root bun.lock fails HERE (fast) instead of only in the
|
|
# Docker build (deploy/Dockerfile), which is what reddened main on #485.
|
|
run: bun install --frozen-lockfile
|
|
|
|
# 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
|
|
|
|
# `cargo check` never compiles #[cfg(test)] code, so without this the
|
|
# shell's unit tests (crash.rs, reset.rs, commands.rs, …) neither build
|
|
# nor run anywhere in CI. --lib scopes it to the unit tests; each
|
|
# matrix target equals its host triple, so the test binary runs
|
|
# natively. Codegen is warmed by the rust-cache above.
|
|
- name: Cargo test (Tauri shell unit tests)
|
|
working-directory: frontend/src-tauri
|
|
run: cargo test --lib --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 }}
|
|
# Priced for a COLD `uv sync`, on every platform.
|
|
#
|
|
# The previous split (Windows 25, Linux/macOS 10) came from a warm-cache
|
|
# measurement — Linux and macOS finish in ~65 s when setup-uv restores its
|
|
# cache, so 10 looked generous. Then run 30439640107 hit
|
|
# "Failed to restore: Cache service responded with 400", Linux installed
|
|
# torch from scratch, and the leg was killed at 10m17s. The 65 s was the
|
|
# cache, not the platform.
|
|
#
|
|
# A cache miss is not rare enough to treat as an outage (GitHub's cache
|
|
# service 400s, a lockfile change invalidates the key, a new runner image
|
|
# starts empty), and a timeout here is self-perpetuating: the leg dies
|
|
# before the post-step saves the cache, so the next run is cold too.
|
|
# 25 everywhere is still bounded — a genuinely wedged job is caught in
|
|
# minutes, not hours — and warm runs land nowhere near it.
|
|
timeout-minutes: 25
|
|
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: |
|
|
# The community chocolatey feed 50x's intermittently (broke PR runs on
|
|
# 2026-07-20 and 2026-07-28) — retry with backoff before failing.
|
|
#
|
|
# Test the OUTCOME, not choco's exit code. On 2026-07-28 the feed
|
|
# returned 503, choco reported "Unable to find package 'ffmpeg'" and
|
|
# "installed 0/0 packages" — and still exited 0. The `&& break` that
|
|
# was supposed to guard this fired on the first attempt, no retry ran,
|
|
# and the job died one line later on `ffmpeg: command not found`.
|
|
# A retry that trusts a lying exit code is not a retry.
|
|
for i in 1 2 3; do
|
|
choco install ffmpeg -y --no-progress || true
|
|
hash -r 2>/dev/null || true
|
|
if command -v ffmpeg >/dev/null 2>&1; then break; fi
|
|
# No backoff after the last attempt — there is no fourth try to
|
|
# wait for, and sleeping 90s only delays an already-doomed job.
|
|
if [ "$i" -eq 3 ]; then
|
|
echo "choco failed to produce ffmpeg after 3 attempts"
|
|
break
|
|
fi
|
|
echo "choco attempt $i did not produce ffmpeg — retrying in $((i * 30))s"
|
|
sleep $((i * 30))
|
|
done
|
|
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
|
|
env:
|
|
HF_HUB_OFFLINE: "1" # same no-silent-downloads guard as the main pytest job
|