Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
215c4fed26 | ||
|
|
92e6a80472 | ||
|
|
990705fe0e | ||
|
|
4dd8a42d3a |
+468
-71
@@ -1,77 +1,474 @@
|
|||||||
|
# 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
|
name: CI
|
||||||
'on':
|
|
||||||
workflow_dispatch: {}
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
windows_wix_diagnostic:
|
||||||
|
description: Run only the tiny nonpublishing Windows MSI authoring diagnostic
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
env:
|
env:
|
||||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: 'true'
|
# Run all JavaScript actions on Node 24 (GH deprecates Node 20 in Sep 2026).
|
||||||
|
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
diagnostic:
|
test:
|
||||||
runs-on: windows-latest
|
if: ${{ !inputs.windows_wix_diagnostic }}
|
||||||
timeout-minutes: 90
|
name: Tests (backend + frontend)
|
||||||
strategy:
|
runs-on: ubuntu-22.04
|
||||||
matrix:
|
env:
|
||||||
rust_target:
|
# Same restricted-network resilience the smoke matrix already sets. This
|
||||||
- x86_64-pc-windows-msvc
|
# job resolves the same direct-URL dependency and had none of it, which
|
||||||
|
# is why it was the one that kept dying (see scripts/uv-sync-retry.sh).
|
||||||
|
UV_HTTP_TIMEOUT: "120"
|
||||||
|
UV_HTTP_RETRIES: "5"
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-python@v5
|
|
||||||
with:
|
- name: Setup Python 3.11
|
||||||
python-version: '3.11'
|
uses: actions/setup-python@v5
|
||||||
- uses: oven-sh/setup-bun@v1
|
with:
|
||||||
- uses: dtolnay/rust-toolchain@stable
|
python-version: "3.11"
|
||||||
with:
|
|
||||||
targets: x86_64-pc-windows-msvc
|
# enable-cache persists ~/.cache/uv across runs, keyed on uv.lock —
|
||||||
- uses: Swatinem/rust-cache@v2
|
# turns `uv sync` from ~45 s cold to ~5 s warm.
|
||||||
with:
|
- name: Install uv
|
||||||
workspaces: frontend/src-tauri -> target
|
uses: astral-sh/setup-uv@v3
|
||||||
- run: bun install --frozen-lockfile
|
with:
|
||||||
- name: Bundle uv (${{ matrix.rust_target }})
|
enable-cache: true
|
||||||
shell: bash
|
cache-dependency-glob: "uv.lock"
|
||||||
env:
|
|
||||||
UV_VERSION: 0.11.7
|
# Node 22 is needed for --experimental-strip-types so node:test can
|
||||||
TRIPLE: ${{ matrix.rust_target }}
|
# import .ts files directly from frontend/src/api/*.
|
||||||
run: "set -euo pipefail\nmkdir -p frontend/src-tauri/binaries\ncase \"$TRIPLE\" in\n aarch64-apple-darwin|x86_64-apple-darwin|x86_64-unknown-linux-gnu)\n\
|
- name: Setup Node 22
|
||||||
\ ARCHIVE=\"tar.gz\"\n ;;\n x86_64-pc-windows-msvc)\n ARCHIVE=\"zip\"\n ;;\n *)\n echo\
|
uses: actions/setup-node@v4
|
||||||
\ \"Unsupported target for uv bundling: $TRIPLE\"\n exit 1\n ;;\nesac\nURL=\"https://github.com/astral-sh/uv/releases/download/${UV_VERSION}/uv-${TRIPLE}.${ARCHIVE}\"\
|
with:
|
||||||
\necho \"Fetching $URL\"\nWORK=$(mktemp -d)\nif [ \"$ARCHIVE\" = \"zip\" ]; then\n curl -fsSL \"$URL\"\
|
node-version: '22'
|
||||||
\ -o \"$WORK/uv.zip\"\n unzip -j -o \"$WORK/uv.zip\" -d \"$WORK\"\n mv \"$WORK/uv.exe\" \"frontend/src-tauri/binaries/uv-${TRIPLE}.exe\"\
|
|
||||||
\nelse\n curl -fsSL \"$URL\" | tar -xz -C \"$WORK\"\n mv \"$WORK/uv-${TRIPLE}/uv\" \"frontend/src-tauri/binaries/uv-${TRIPLE}\"\
|
- name: Setup Bun
|
||||||
\n chmod +x \"frontend/src-tauri/binaries/uv-${TRIPLE}\"\nfi\nls -la \"frontend/src-tauri/binaries/\"\n"
|
uses: oven-sh/setup-bun@v1
|
||||||
- name: Bundle ffmpeg + ffprobe (${{ matrix.rust_target }})
|
|
||||||
shell: bash
|
# apt install ffmpeg is ~30 s every run; cache the resolved .debs.
|
||||||
env:
|
- name: System deps (ffmpeg)
|
||||||
TRIPLE: ${{ matrix.rust_target }}
|
uses: awalsh128/cache-apt-pkgs-action@v1.6.3
|
||||||
FFMPEG_BTBN_VERSION: latest
|
with:
|
||||||
run: "set -euo pipefail\nBINDIR=\"frontend/src-tauri/binaries\"\nmkdir -p \"$BINDIR\"\nWORK=$(mktemp -d)\n\
|
packages: ffmpeg
|
||||||
\ncase \"$TRIPLE\" in\n aarch64-apple-darwin|x86_64-apple-darwin)\n # evermeet.cx ships each binary\
|
version: 1.0
|
||||||
\ as a separate .zip containing\n # a single x86_64 Mach-O executable \u2014 natively correct on the\n\
|
|
||||||
\ # Intel leg, and runs via Rosetta 2 on the arm64 leg. Both\n # darwin TRIPLEs therefore bundle the\
|
- name: Install Python deps
|
||||||
\ same payload; only the\n # sidecar filename suffix differs.\n for TOOL in ffmpeg ffprobe; do\n \
|
# `--all-extras` installs optional engine deps (e.g. `supertonic`)
|
||||||
\ if [ \"$TOOL\" = \"ffmpeg\" ]; then\n URL=\"https://evermeet.cx/ffmpeg/getrelease/zip\"\n \
|
# so their tests can exercise the real import path, not the
|
||||||
\ else\n URL=\"https://evermeet.cx/ffmpeg/getrelease/${TOOL}/zip\"\n fi\n echo \"Fetching\
|
# "package not installed" fallback. Smoke job below stays on bare
|
||||||
\ $TOOL from evermeet.cx\"\n curl -fsSL \"$URL\" -o \"$WORK/${TOOL}.zip\"\n unzip -o -j \"$WORK/${TOOL}.zip\"\
|
# `uv sync` because smoke only hits /health + fixture profiles.
|
||||||
\ -d \"$WORK\"\n mv \"$WORK/${TOOL}\" \"$BINDIR/${TOOL}-${TRIPLE}\"\n chmod +x \"$BINDIR/${TOOL}-${TRIPLE}\"\
|
#
|
||||||
\n done\n ;;\n x86_64-unknown-linux-gnu)\n URL=\"https://github.com/BtbN/FFmpeg-Builds/releases/download/${FFMPEG_BTBN_VERSION}/ffmpeg-master-${FFMPEG_BTBN_VERSION}-linux64-gpl.tar.xz\"\
|
# Retried because one dependency — en-core-web-sm — resolves to a
|
||||||
\n echo \"Fetching ffmpeg from BtbN (linux64) \u2014 version=${FFMPEG_BTBN_VERSION}\"\n curl -fsSL\
|
# direct GitHub release URL, and github.com intermittently answers
|
||||||
\ \"$URL\" -o \"$WORK/ffmpeg.tar.xz\"\n tar -xJf \"$WORK/ffmpeg.tar.xz\" -C \"$WORK\"\n # Archive\
|
# `http2 error: refused stream before processing any application
|
||||||
\ extracts to ffmpeg-master-latest-linux64-gpl/bin/\n EXTRACTED=$(find \"$WORK\" -type d -name \"bin\"\
|
# logic`. uv's own 3 retries all land inside the same few seconds and
|
||||||
\ | head -1)\n mv \"$EXTRACTED/ffmpeg\" \"$BINDIR/ffmpeg-${TRIPLE}\"\n mv \"$EXTRACTED/ffprobe\"\
|
# fail together, which has cost otherwise-green runs (#1517, #1518).
|
||||||
\ \"$BINDIR/ffprobe-${TRIPLE}\"\n chmod +x \"$BINDIR/ffmpeg-${TRIPLE}\" \"$BINDIR/ffprobe-${TRIPLE}\"\
|
# Backing off between whole attempts is what actually clears it.
|
||||||
\n ;;\n x86_64-pc-windows-msvc)\n URL=\"https://github.com/BtbN/FFmpeg-Builds/releases/download/${FFMPEG_BTBN_VERSION}/ffmpeg-master-${FFMPEG_BTBN_VERSION}-win64-gpl.zip\"\
|
run: bash scripts/uv-sync-retry.sh --all-extras
|
||||||
\n echo \"Fetching ffmpeg from BtbN (win64) \u2014 version=${FFMPEG_BTBN_VERSION}\"\n curl -fsSL \"\
|
|
||||||
$URL\" -o \"$WORK/ffmpeg.zip\"\n unzip -o \"$WORK/ffmpeg.zip\" -d \"$WORK\"\n EXTRACTED=$(find \"\
|
# HF_HUB_OFFLINE=1 is a recurrence guard, not an optimization: a test
|
||||||
$WORK\" -type f -name \"ffmpeg.exe\" | head -1)\n EXTRACTED_DIR=$(dirname \"$EXTRACTED\")\n mv \"\
|
# that reaches huggingface.co fails fast and loud instead of silently
|
||||||
$EXTRACTED_DIR/ffmpeg.exe\" \"$BINDIR/ffmpeg-${TRIPLE}.exe\"\n mv \"$EXTRACTED_DIR/ffprobe.exe\" \"\
|
# downloading model weights mid-suite (the preload_model() Hub-probe
|
||||||
$BINDIR/ffprobe-${TRIPLE}.exe\"\n ;;\n *)\n echo \"\u26A0 No ffmpeg bundling for target: $TRIPLE\
|
# bug pulled the full 2.3 GB k2-fsa/OmniVoice checkpoint into every
|
||||||
\ (will download at first run)\"\n ;;\nesac\nls -la \"$BINDIR/\"\n"
|
# networked empty-cache run before it was caught). All legitimate HF
|
||||||
- name: Reproduce full-resource MSI build twice
|
# interactions in tests are stubbed; anything that trips this is a
|
||||||
shell: pwsh
|
# test-isolation bug.
|
||||||
run: ./scripts/diagnose-full-windows-wix.ps1
|
- name: Run pytest
|
||||||
- name: Preserve authoring diagnostics
|
run: uv run --no-sync pytest tests/ -q --tb=short
|
||||||
if: always()
|
env:
|
||||||
uses: actions/upload-artifact@v4
|
HF_HUB_OFFLINE: "1"
|
||||||
with:
|
|
||||||
name: full-windows-wix-diagnostics
|
# Docs-drift CI gate (Phase 1 INST-06). The validator extracts code
|
||||||
path: full-wix-diagnostic-artifacts/
|
# blocks tagged `<!-- validate -->` from docs/install/*.md and asserts
|
||||||
retention-days: 3
|
# 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
|
||||||
|
bash scripts/inject-apprun.test.sh
|
||||||
|
bash scripts/verify-apprun-bundle.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 --no-sync 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:
|
||||||
|
if: ${{ !inputs.windows_wix_diagnostic }}
|
||||||
|
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
|
||||||
|
|
||||||
|
# Backend-lifecycle fault-injection harness: real child processes die
|
||||||
|
# scripted deaths through the OMNIVOICE_BACKEND_CMD seam, and each
|
||||||
|
# scenario asserts the user-visible diagnosis names the actual cause
|
||||||
|
# (port conflict / traceback root cause / spawn failure / timeout /
|
||||||
|
# crash-loop exhaustion / signal 9 / deliberate replace / deferred-
|
||||||
|
# startup step). Serial: the scenarios share process-global state
|
||||||
|
# (env vars, crash store, kill-intended flag) by design.
|
||||||
|
- name: Cargo test (backend lifecycle harness)
|
||||||
|
working-directory: frontend/src-tauri
|
||||||
|
run: cargo test --test backend_lifecycle --target ${{ matrix.rust_target }} --message-format=short -- --test-threads=1
|
||||||
|
|
||||||
|
# ── 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:
|
||||||
|
if: ${{ !inputs.windows_wix_diagnostic }}
|
||||||
|
name: Smoke (${{ matrix.label }})
|
||||||
|
needs: test
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- os: macos-14
|
||||||
|
label: macOS
|
||||||
|
backend_supported: true
|
||||||
|
- os: macos-15-intel
|
||||||
|
label: macOS Intel
|
||||||
|
backend_supported: false
|
||||||
|
- os: windows-2022
|
||||||
|
label: Windows
|
||||||
|
backend_supported: true
|
||||||
|
- os: ubuntu-22.04
|
||||||
|
label: Linux
|
||||||
|
backend_supported: true
|
||||||
|
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' && matrix.backend_supported
|
||||||
|
run: brew install ffmpeg libsndfile || true
|
||||||
|
|
||||||
|
- name: System deps (Windows)
|
||||||
|
if: runner.os == 'Windows' && matrix.backend_supported
|
||||||
|
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
|
||||||
|
# Chocolatey is one distribution channel, not the dependency. When
|
||||||
|
# its feed is down across every retry (2026-08-13: three attempts,
|
||||||
|
# three 'installed 0/1'), fall back to the static gyan.dev release
|
||||||
|
# build GitHub mirror — the same binary, no feed in the path.
|
||||||
|
if ! command -v ffmpeg >/dev/null 2>&1; then
|
||||||
|
echo "::warning::choco feed down — falling back to static ffmpeg build"
|
||||||
|
curl -fsSL --retry 3 -o /tmp/ffmpeg.zip \
|
||||||
|
https://github.com/GyanD/codexffmpeg/releases/download/7.1/ffmpeg-7.1-essentials_build.zip
|
||||||
|
unzip -q /tmp/ffmpeg.zip -d /tmp/ffmpeg
|
||||||
|
bindir=$(dirname "$(find /tmp/ffmpeg -name ffmpeg.exe | head -1)")
|
||||||
|
echo "$bindir" >> "$GITHUB_PATH"
|
||||||
|
export PATH="$bindir:$PATH"
|
||||||
|
fi
|
||||||
|
ffmpeg -version
|
||||||
|
|
||||||
|
- name: System deps (Linux)
|
||||||
|
if: runner.os == 'Linux' && matrix.backend_supported
|
||||||
|
uses: awalsh128/cache-apt-pkgs-action@v1.6.3
|
||||||
|
with:
|
||||||
|
packages: ffmpeg libsndfile1
|
||||||
|
version: 1.0
|
||||||
|
|
||||||
|
- name: Install Python deps (including PocketTTS)
|
||||||
|
# PocketTTS is an opt-in engine, but installing its pinned extra here
|
||||||
|
# proves that the same dependency set resolves on every supported local
|
||||||
|
# backend host. The Intel-Mac leg separately pins the documented
|
||||||
|
# unsupported contract: its UI is a remote-backend client only (#889).
|
||||||
|
if: matrix.backend_supported
|
||||||
|
run: bash scripts/uv-sync-retry.sh --extra pockettts
|
||||||
|
|
||||||
|
- name: Verify the documented Intel Mac contract
|
||||||
|
if: ${{ !matrix.backend_supported }}
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
python3 - <<'PY'
|
||||||
|
from pathlib import Path
|
||||||
|
import platform
|
||||||
|
import tomllib
|
||||||
|
|
||||||
|
assert platform.system() == "Darwin"
|
||||||
|
assert platform.machine() == "x86_64"
|
||||||
|
root = Path.cwd()
|
||||||
|
project = tomllib.loads((root / "pyproject.toml").read_text("utf-8"))
|
||||||
|
extra = project["project"]["optional-dependencies"]["pockettts"]
|
||||||
|
assert extra == [
|
||||||
|
"pocket-tts==2.1.0 ; sys_platform != 'darwin' or platform_machine != 'x86_64'"
|
||||||
|
]
|
||||||
|
docs = (root / "docs/install/macos.md").read_text("utf-8")
|
||||||
|
assert "Intel Macs are not supported" in docs
|
||||||
|
PY
|
||||||
|
|
||||||
|
- name: Run smoke tests
|
||||||
|
# Exercise credential paths on native Windows as well as POSIX hosts.
|
||||||
|
if: matrix.backend_supported
|
||||||
|
run: uv run --no-sync pytest tests/smoke/ tests/test_hf_token_cache_paths.py -q --tb=short
|
||||||
|
env:
|
||||||
|
HF_HUB_OFFLINE: "1" # same no-silent-downloads guard as the main pytest job
|
||||||
|
HF_HUB_CACHE: ${{ runner.temp }}/pockettts-empty-hf-cache
|
||||||
|
|
||||||
|
# Artifact commits depend on native Windows rename/replace semantics;
|
||||||
|
# Linux emulation cannot exercise sharing rules or path parsing.
|
||||||
|
- name: Remote-worker artifact paths (Windows)
|
||||||
|
if: runner.os == 'Windows' && matrix.backend_supported
|
||||||
|
run: uv run --no-sync pytest tests/test_worker_upload_server.py tests/test_worker_server_integrity.py -q --tb=short
|
||||||
|
env:
|
||||||
|
HF_HUB_OFFLINE: "1"
|
||||||
|
HF_HUB_CACHE: ${{ runner.temp }}/worker-artifact-empty-hf-cache
|
||||||
|
|
||||||
|
windows-wix-diagnostic:
|
||||||
|
name: Windows MSI authoring (no publishing)
|
||||||
|
needs: test
|
||||||
|
if: ${{ !cancelled() && (inputs.windows_wix_diagnostic || needs.test.result == 'success') }}
|
||||||
|
runs-on: windows-2022
|
||||||
|
timeout-minutes: 15
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: oven-sh/setup-bun@v1
|
||||||
|
- name: Bundle canonical system and per-user templates with a tiny payload
|
||||||
|
shell: pwsh
|
||||||
|
run: ./scripts/diagnose-windows-wix.ps1
|
||||||
|
- name: Preserve verbose linker output and rendered authoring
|
||||||
|
if: always()
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: windows-wix-diagnostic
|
||||||
|
path: wix-diagnostic-artifacts/
|
||||||
|
if-no-files-found: warn
|
||||||
|
retention-days: 3
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ the frozen-backend fallback mirror it for their toolchains.
|
|||||||
- Tilde-separated number ranges are spoken clearly without running their endpoints together (#1821) — thanks @flutterkage2k!
|
- Tilde-separated number ranges are spoken clearly without running their endpoints together (#1821) — thanks @flutterkage2k!
|
||||||
- Voice modes use themed tabs, with Synthesize and Convert pinned below their scrolling forms (#1823)
|
- Voice modes use themed tabs, with Synthesize and Convert pinned below their scrolling forms (#1823)
|
||||||
- Fix current-user Windows installer validation and nested resource cleanup (#1873)
|
- Fix current-user Windows installer validation and nested resource cleanup (#1873)
|
||||||
|
- Keep generated frontend assets available while building the current-user Windows installer (#1873)
|
||||||
|
|
||||||
- Voice cloning now starts with a clear upload-or-record choice, reveals recording and reference details only when needed, and keeps sampling controls under Production Overrides (#1817)
|
- Voice cloning now starts with a clear upload-or-record choice, reveals recording and reference details only when needed, and keeps sampling controls under Production Overrides (#1817)
|
||||||
- The first-run welcome line uses an instruction accepted by OmniVoice and VoiceDesign engines (#1861) — thanks @psiberfunk!
|
- The first-run welcome line uses an instruction accepted by OmniVoice and VoiceDesign engines (#1861) — thanks @psiberfunk!
|
||||||
|
|||||||
@@ -308,8 +308,15 @@ stable per-user component identities, HKCU registry keypaths, and uninstall
|
|||||||
cleanup for nested resource folders. Missing or unrendered resources fail the
|
cleanup for nested resource folders. Missing or unrendered resources fail the
|
||||||
build. The canonical system installer retains its per-machine authoring.
|
build. The canonical system installer retains its per-machine authoring.
|
||||||
|
|
||||||
CI bundles both scopes with a tiny executable, an external helper, and nested
|
The per-user build reuses the system build's frontend output: its config clears
|
||||||
resources using the CLI version locked in `bun.lock`. The Windows MSI authoring
|
`beforeBuildCommand` so a second Vite build cannot replace the hashed files
|
||||||
|
referenced by the rendered WiX template. Keep using `tauri build` for the
|
||||||
|
per-user stage; it still recompiles the shell with its own product configuration.
|
||||||
|
|
||||||
|
CI builds both scopes with a tiny executable, an external helper, and nested
|
||||||
|
resources using the CLI version locked in `bun.lock`. Its frontend-like build
|
||||||
|
hook rotates resource filenames, verifying the per-user build preserves the
|
||||||
|
system build's resource snapshot. The Windows MSI authoring
|
||||||
job runs after the test suite and preserves verbose WiX logs and rendered XML.
|
job runs after the test suite and preserves verbose WiX logs and rendered XML.
|
||||||
For focused diagnosis, dispatch CI with `windows_wix_diagnostic=true`; it skips
|
For focused diagnosis, dispatch CI with `windows_wix_diagnostic=true`; it skips
|
||||||
the other jobs and never signs, publishes, or installs the fixture bundles.
|
the other jobs and never signs, publishes, or installs the fixture bundles.
|
||||||
|
|||||||
@@ -1,47 +0,0 @@
|
|||||||
# Nonpublishing full production-resource reproduction. No release API or signing keys.
|
|
||||||
$ErrorActionPreference = 'Stop'
|
|
||||||
$repo = Split-Path -Parent $PSScriptRoot
|
|
||||||
$artifacts = Join-Path $repo 'full-wix-diagnostic-artifacts'
|
|
||||||
New-Item -ItemType Directory -Force $artifacts | Out-Null
|
|
||||||
$target = 'x86_64-pc-windows-msvc'
|
|
||||||
Push-Location "$repo/frontend"
|
|
||||||
try {
|
|
||||||
'{"bundle":{"createUpdaterArtifacts":false}}' | Set-Content -Encoding utf8 'diagnostic-unsigned.json'
|
|
||||||
foreach ($scope in @('system', 'per-user-original', 'per-user-restored-resources')) {
|
|
||||||
if ($scope -eq 'per-user-original') {
|
|
||||||
& python ../scripts/render-per-user-wix.py --source src-tauri/wix/main.wxs --system-wxs "src-tauri/target/$target/release/wix/x64/main.wxs" --output src-tauri/target/wix-per-user/main.wxs
|
|
||||||
if ($LASTEXITCODE -ne 0) { throw 'Renderer failed' }
|
|
||||||
Copy-Item dist "$artifacts/system-dist" -Recurse
|
|
||||||
}
|
|
||||||
if ($scope -eq 'per-user-restored-resources') {
|
|
||||||
Remove-Item dist -Recurse -Force
|
|
||||||
Copy-Item "$artifacts/system-dist" dist -Recurse
|
|
||||||
}
|
|
||||||
$log = "$artifacts/$scope.log"
|
|
||||||
$ErrorActionPreference = 'Continue'
|
|
||||||
if ($scope -eq 'system') {
|
|
||||||
# Mirror release-only build input without using any secret.
|
|
||||||
$env:VITE_POSTHOG_KEY = 'diagnostic-placeholder'
|
|
||||||
& bun x tauri build -vv --target $target --bundles msi --config diagnostic-unsigned.json *> $log
|
|
||||||
} elseif ($scope -eq 'per-user-original') {
|
|
||||||
Remove-Item Env:VITE_POSTHOG_KEY -ErrorAction SilentlyContinue
|
|
||||||
& bun x tauri build -vv --target $target --bundles msi --config src-tauri/tauri.per-user.conf.json --config diagnostic-unsigned.json *> $log
|
|
||||||
} else {
|
|
||||||
& bun x tauri bundle -vv --target $target --bundles msi --config src-tauri/tauri.per-user.conf.json --config diagnostic-unsigned.json *> $log
|
|
||||||
}
|
|
||||||
$code = $LASTEXITCODE
|
|
||||||
$ErrorActionPreference = 'Stop'
|
|
||||||
Get-Content $log
|
|
||||||
"$scope=$code" | Add-Content "$artifacts/results.txt"
|
|
||||||
$capture = "$artifacts/$scope"
|
|
||||||
New-Item -ItemType Directory -Force $capture | Out-Null
|
|
||||||
Get-ChildItem "src-tauri/target/$target/release" -Recurse -File |
|
|
||||||
Where-Object { $_.Extension -in @('.wxs','.wxl','.wixobj','.wixpdb') } |
|
|
||||||
Copy-Item -Destination $capture -Force
|
|
||||||
Get-ChildItem dist/assets -File | Select-Object Name,Length | ConvertTo-Json | Set-Content "$capture/dist-files.json"
|
|
||||||
if ($scope -eq 'system' -and $code -ne 0) { throw 'System baseline failed' }
|
|
||||||
if ($scope -eq 'per-user-restored-resources' -and $code -ne 0) { throw 'Restored-resource bundle failed' }
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
Pop-Location
|
|
||||||
}
|
|
||||||
@@ -9,14 +9,33 @@ $lock = Get-Content "$repo/bun.lock" -Raw
|
|||||||
$cliMatch = [regex]::Match($lock, '"@tauri-apps/cli":\s*\["@tauri-apps/cli@([^"\s]+)"')
|
$cliMatch = [regex]::Match($lock, '"@tauri-apps/cli":\s*\["@tauri-apps/cli@([^"\s]+)"')
|
||||||
if (-not $cliMatch.Success) { throw 'Cannot resolve the Tauri CLI version from bun.lock' }
|
if (-not $cliMatch.Success) { throw 'Cannot resolve the Tauri CLI version from bun.lock' }
|
||||||
$cliPackage = '@tauri-apps/cli@' + $cliMatch.Groups[1].Value
|
$cliPackage = '@tauri-apps/cli@' + $cliMatch.Groups[1].Value
|
||||||
New-Item -ItemType Directory -Force -Path $fixture, $artifacts, "$fixture/src", "$fixture/resources/nested", "$fixture/binaries" | Out-Null
|
New-Item -ItemType Directory -Force -Path $fixture, $artifacts, "$fixture/src", "$fixture/resources/nested", "$fixture/binaries", "$fixture/tauri-stub/src" | Out-Null
|
||||||
@'
|
@'
|
||||||
[package]
|
[package]
|
||||||
name = "wix-diagnostic"
|
name = "wix-diagnostic"
|
||||||
version = "0.0.0"
|
version = "0.0.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
[dependencies]
|
||||||
|
tauri = { path = "tauri-stub" }
|
||||||
[workspace]
|
[workspace]
|
||||||
'@ | Set-Content -Encoding utf8 "$fixture/Cargo.toml"
|
'@ | Set-Content -Encoding utf8 "$fixture/Cargo.toml"
|
||||||
|
@'
|
||||||
|
[package]
|
||||||
|
name = "tauri"
|
||||||
|
version = "2.0.0"
|
||||||
|
edition = "2021"
|
||||||
|
[features]
|
||||||
|
custom-protocol = []
|
||||||
|
'@ | Set-Content -Encoding utf8 "$fixture/tauri-stub/Cargo.toml"
|
||||||
|
'' | Set-Content -Encoding utf8 "$fixture/tauri-stub/src/lib.rs"
|
||||||
|
@'
|
||||||
|
from pathlib import Path
|
||||||
|
import uuid
|
||||||
|
root = Path(__file__).parent / "resources" / "nested"
|
||||||
|
for previous in root.glob("asset-*.txt"):
|
||||||
|
previous.unlink()
|
||||||
|
(root / f"asset-{uuid.uuid4().hex}.txt").write_text("changing frontend-like resource")
|
||||||
|
'@ | Set-Content -Encoding utf8 "$fixture/build-assets.py"
|
||||||
'fn main() { println!("MSI authoring diagnostic only"); }' | Set-Content -Encoding utf8 "$fixture/src/main.rs"
|
'fn main() { println!("MSI authoring diagnostic only"); }' | Set-Content -Encoding utf8 "$fixture/src/main.rs"
|
||||||
'Nested resource payload' | Set-Content -Encoding utf8 "$fixture/resources/nested/payload.txt"
|
'Nested resource payload' | Set-Content -Encoding utf8 "$fixture/resources/nested/payload.txt"
|
||||||
'Root resource payload' | Set-Content -Encoding utf8 "$fixture/resources/readme.txt"
|
'Root resource payload' | Set-Content -Encoding utf8 "$fixture/resources/readme.txt"
|
||||||
@@ -26,7 +45,7 @@ $config = @{
|
|||||||
productName = 'VoiceStudio MSI Diagnostic'
|
productName = 'VoiceStudio MSI Diagnostic'
|
||||||
version = '0.0.0'
|
version = '0.0.0'
|
||||||
identifier = 'com.debpalash.voicestudio.wixdiagnostic'
|
identifier = 'com.debpalash.voicestudio.wixdiagnostic'
|
||||||
build = @{}
|
build = @{ beforeBuildCommand = @{ script = 'python build-assets.py'; cwd = $fixture } }
|
||||||
bundle = @{
|
bundle = @{
|
||||||
active = $true
|
active = $true
|
||||||
targets = @('msi')
|
targets = @('msi')
|
||||||
@@ -57,10 +76,14 @@ try {
|
|||||||
productName = "VoiceStudio MSI Diagnostic $scope"
|
productName = "VoiceStudio MSI Diagnostic $scope"
|
||||||
bundle = @{ windows = @{ wix = @{ template = "$scope.wxs" } } }
|
bundle = @{ windows = @{ wix = @{ template = "$scope.wxs" } } }
|
||||||
}
|
}
|
||||||
|
if ($scope -eq 'per-user') {
|
||||||
|
$production = Get-Content "$repo/frontend/src-tauri/tauri.per-user.conf.json" -Raw | ConvertFrom-Json -AsHashtable
|
||||||
|
if ($production.ContainsKey('build')) { $scopeConfig.build = $production.build }
|
||||||
|
}
|
||||||
$scopeConfig | ConvertTo-Json -Depth 20 | Set-Content -Encoding utf8 "$fixture/$scope.conf.json"
|
$scopeConfig | ConvertTo-Json -Depth 20 | Set-Content -Encoding utf8 "$fixture/$scope.conf.json"
|
||||||
$log = Join-Path $artifacts "$scope.log"
|
$log = Join-Path $artifacts "$scope.log"
|
||||||
$ErrorActionPreference = 'Continue'
|
$ErrorActionPreference = 'Continue'
|
||||||
& bun x --package $cliPackage tauri bundle -vv --target $target --bundles msi --config "$scope.conf.json" *> $log
|
& bun x --package $cliPackage tauri build -vv --target $target --bundles msi --config "$scope.conf.json" *> $log
|
||||||
$results[$scope] = $LASTEXITCODE
|
$results[$scope] = $LASTEXITCODE
|
||||||
$ErrorActionPreference = 'Stop'
|
$ErrorActionPreference = 'Stop'
|
||||||
Get-Content $log
|
Get-Content $log
|
||||||
|
|||||||
@@ -52,6 +52,9 @@ def test_machine_and_per_user_templates_have_distinct_scopes_and_roots():
|
|||||||
def test_per_user_bundle_has_separate_identity_and_no_elevated_update_task():
|
def test_per_user_bundle_has_separate_identity_and_no_elevated_update_task():
|
||||||
config = json.loads(CONFIG.read_text(encoding="utf-8"))
|
config = json.loads(CONFIG.read_text(encoding="utf-8"))
|
||||||
assert config["productName"].endswith("(Current User)")
|
assert config["productName"].endswith("(Current User)")
|
||||||
|
# The WiX resource snapshot comes from the preceding system build.
|
||||||
|
# Null deletes the inherited hook via Tauri's JSON Merge Patch.
|
||||||
|
assert config["build"]["beforeBuildCommand"] is None
|
||||||
wix = config["bundle"]["windows"]["wix"]
|
wix = config["bundle"]["windows"]["wix"]
|
||||||
assert wix["upgradeCode"] == "f27de3a8-a9dc-4a3d-84bb-e98f1bf82393"
|
assert wix["upgradeCode"] == "f27de3a8-a9dc-4a3d-84bb-e98f1bf82393"
|
||||||
assert wix["enableElevatedUpdateTask"] is False
|
assert wix["enableElevatedUpdateTask"] is False
|
||||||
|
|||||||
Reference in New Issue
Block a user