mpv now decodes video on Linux, drawn into a framebuffer we own and blitted
into the default vbox's draw handler. Tauri's widget tree is untouched, so an
upgrade that assumes its own layout cannot invalidate this. Direct play means
the original file, hardware decoding, and no server transcode at all — where
previously every desktop video was re-encoded to h264 for the browser engine,
whatever the file actually was. Off by default: JELLYTAU_NATIVE_VIDEO=1.
That settles finding 2 of playback-backend-unification.md — "native video
cannot be composited with a Tauri webview" — by demonstration rather than
argument, on X11 and Wayland both.
Turning it on exposed nine defects, none of them mpv's. Each was the same
mistake in a different place: a capability written down as a compile-time fact
about the platform, or a state asserted instead of confirmed.
DR-238/246 a seek routed by the stream's container rather than by what the
engine could do with it - correct only while one player handled
those streams, silent the moment another did
DR-239 a property handled but never observed, so the play/pause button
waited for an event that could not arrive
DR-240 fullscreen expanding the document while the window stayed put
DR-241 a seek issued before the engine had a file, failed, and discarded
- which is why resume began at zero
DR-247 a Linux-only gate outliving the caller that made it Linux-only,
breaking the Android build outright
DR-250 a stop aimed at whichever renderer bookkeeping believed was in
charge, missing the one actually making sound
DR-251 a duration of zero believed, leaving the seek bar no scale
DR-252 a junk float converted to a Duration, panicking the backend the
instant a length-less stream appeared
So the MediaPlayer contract (DR-242 … DR-247): `open` carries a start position,
so no caller sequences load-then-seek and none can race an engine's load;
`seek` states a destination and leaves in-place-versus-re-open to the engine;
`snapshot` is one coherent read; and `Phase::Opening` names the window where
intent used to be lost. One conformance suite runs against every engine —
FakePlayer and mpv under cargo test, ExoPlayer instrumented on a device — so an
engine is either correct or visibly failing.
Two of the nine were introduced during this work and caught on hardware, not by
any suite: an over-broad capability that grouped ExoPlayer with mpv, and the
Duration panic. The suites test engines that behave. That is recorded in
docs/native-player-verification.md, which asks for the exact action sequences
that found them.
Verified: all automated gates, conformance (mpv 9/9, legacy 8/9 by design,
ExoPlayer 7/7 on device), and manual desktop and Android passes on real
hardware.
Known open and deliberately shipped: resume reads local progress and never the
server's; the background-audio handoff still declares a state swap it does not
confirm (the symptom is now impossible, the race is not); and `bun run
android:dev` builds an APK carrying the release application id, whose failure
message advises an uninstall that would destroy app data. Fix that last one
before anyone else builds for Android.
Squashed from worktree-linux-native-video, which keeps the per-defect history.
299 lines
13 KiB
YAML
299 lines
13 KiB
YAML
name: '🏗️ Build and Test JellyTau'
|
|
|
|
on:
|
|
push:
|
|
branches:
|
|
- master
|
|
paths-ignore:
|
|
- '**/*.md'
|
|
pull_request:
|
|
branches:
|
|
- master
|
|
paths-ignore:
|
|
- '**/*.md'
|
|
workflow_dispatch:
|
|
|
|
env:
|
|
# Incremental state is never reused between CI runs -- pure disk cost.
|
|
CARGO_INCREMENTAL: 0
|
|
|
|
jobs:
|
|
test:
|
|
name: Run Tests
|
|
# A release push triggers build-release.yml on the tag, which runs this exact
|
|
# test suite itself — and on a single-slot runner the two ~1h workflows would
|
|
# otherwise serialize/contend. Skip the duplicate for chore(release) commits.
|
|
# (head_commit is absent on pull_request/workflow_dispatch; startsWith(null,…)
|
|
# is false there, so those events still run.)
|
|
if: "!startsWith(github.event.head_commit.message, 'chore(release)')"
|
|
runs-on: linux/amd64
|
|
container:
|
|
image: gitea.tourolle.paris/dtourolle/jellytau-builder:2026.08.1
|
|
|
|
steps:
|
|
- name: Checkout repository
|
|
uses: actions/checkout@v4
|
|
|
|
- name: Cache Rust dependencies
|
|
uses: actions/cache@v3
|
|
with:
|
|
# Registry only -- never src-tauri/target. That directory is ~16 GB and
|
|
# was cached under five separate keys, which filled the runner's 74 GB
|
|
# disk at ~1.15 GB/day (23 GB in 20 days, measured Aug 2026).
|
|
# registry/src is omitted too: cargo re-extracts it for free from
|
|
# registry/cache (155 MB of .crate tarballs vs 1.1 GB extracted).
|
|
path: |
|
|
~/.cargo/registry/index
|
|
~/.cargo/registry/cache
|
|
~/.cargo/git/db
|
|
# One shared key across every job. The old per-job keys existed to stop
|
|
# debug/release target artifacts clobbering each other; with target no
|
|
# longer cached, registry contents are target-independent, so all jobs
|
|
# want the same crates. First job to finish saves; the rest restore.
|
|
key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
|
|
restore-keys: |
|
|
${{ runner.os }}-cargo-registry-
|
|
|
|
- name: Cache Node dependencies
|
|
uses: actions/cache@v3
|
|
with:
|
|
path: |
|
|
~/.bun/install/cache
|
|
node_modules
|
|
key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock') }}
|
|
restore-keys: |
|
|
${{ runner.os }}-bun-
|
|
|
|
- name: Install dependencies
|
|
run: |
|
|
bun install
|
|
|
|
# Tripwire for domain-taxonomy leaks into the presentation layer (a
|
|
# multi-type includeItemTypes query defining a category in the frontend).
|
|
# See scripts/check-frontend-boundary.sh and
|
|
# docs/specs/scoped-search-boundary.md.
|
|
- name: Check frontend/backend boundary
|
|
run: bash scripts/check-frontend-boundary.sh
|
|
|
|
# The docs are the maintained source of truth for architecture and
|
|
# process, and they cross-reference each other heavily. A rename that
|
|
# misses a link turns a doc into a dead end silently. Pure shell + git —
|
|
# no tool is installed at job time.
|
|
- name: Check documentation links
|
|
run: bash scripts/check-doc-links.sh
|
|
|
|
# Formatting, linting and type-checking were all configured in this repo
|
|
# and enforced by nothing: .prettierrc described a tree where 199 files did
|
|
# not match it, eslint.config.js ran in no workflow and in no hook, and
|
|
# `bun run check` ran only in build-release.yml — i.e. a type error could
|
|
# sit on master until somebody cut a tag. These three steps are what make
|
|
# those configs load-bearing. All are project deps installed by
|
|
# `bun install`; nothing is fetched at job time.
|
|
# Cheap tripwire for a class of defect this repo kept hitting: tooling on
|
|
# a rarely-taken path. scripts/build-android.sh ran `npm install` on its
|
|
# clean-build branch -- in a bun project, ignoring bun.lock and
|
|
# re-resolving the tree, which is how the Tauri plugin crate/package
|
|
# versions drifted apart and broke a release build. It survived because
|
|
# clean builds are rare.
|
|
- name: Check build tooling
|
|
run: bash scripts/check-tooling.sh
|
|
|
|
- name: Check formatting
|
|
run: bun run format:check
|
|
|
|
# RATCHET — this number only ever goes DOWN. Same policy as MIN_THRESHOLD
|
|
# in traceability-check.yml and the coverage thresholds in
|
|
# vitest.config.ts. 159 is what the tree carried when the gate went in; the
|
|
# backlog is real findings (dead bindings, unkeyed {#each}, `any` at the
|
|
# IPC boundary) that eslint.config.js documents rule by rule, each parked
|
|
# at "warn" until its class is cleared and it can be promoted to "error".
|
|
# Lower this as you clear them. Never raise it to make a build pass.
|
|
- name: Lint
|
|
run: bun run lint -- --max-warnings=158
|
|
|
|
- name: Check TypeScript
|
|
run: |
|
|
bunx svelte-kit sync
|
|
bun run check
|
|
|
|
# Tauri refuses to build when a plugin's Rust crate and npm package are on
|
|
# different minor versions. Nothing here runs `tauri build` -- that only
|
|
# happens on a tag -- so a mismatch introduced on master stayed invisible
|
|
# until the release build, which is where it was found: v0.10.0 prep hit
|
|
# `tauri-plugin-log (v2.8.0) : @tauri-apps/plugin-log (v2.9.0)`. `cargo
|
|
# check`, clippy, the tests and svelte-check had all passed.
|
|
#
|
|
# `tauri info` performs the same comparison the bundler does, without a
|
|
# build. Grepping its output is crude, but the alternative is discovering
|
|
# this at tag time again.
|
|
- name: Check Tauri plugin versions match
|
|
run: |
|
|
set -e
|
|
if bunx tauri info 2>&1 | tee /tmp/tauri-info.txt | grep -q "version mismatched"; then
|
|
echo "::error::A Tauri plugin's Rust crate and npm package versions disagree."
|
|
echo "::error::The release build will refuse to start. Align them in"
|
|
echo "::error::src-tauri/Cargo.toml and package.json (both are pinned exactly)."
|
|
grep -A6 "version mismatched" /tmp/tauri-info.txt || true
|
|
exit 1
|
|
fi
|
|
echo "✅ Tauri plugin crate/package versions agree."
|
|
|
|
# Coverage rather than a bare `bun run test`: same suite, plus the
|
|
# thresholds in vitest.config.ts, so a large untested module or a deleted
|
|
# test fails here instead of being noticed months later.
|
|
- name: Run frontend tests
|
|
run: |
|
|
bunx svelte-kit sync
|
|
bun run test:coverage
|
|
|
|
# CLAUDE.md has required `cargo fmt` + `cargo clippy` before every commit
|
|
# for as long as the rule has existed, but nothing in CI checked either,
|
|
# so the requirement rested entirely on memory. Both components are baked
|
|
# into the builder image (Dockerfile.builder: `rustup component add
|
|
# rustfmt clippy`) — nothing is installed at job time.
|
|
- name: Check Rust formatting
|
|
run: |
|
|
cd src-tauri
|
|
cargo fmt --all -- --check
|
|
|
|
# Clippy is a hard gate. It was advisory while the tree carried a warning
|
|
# backlog; that backlog is gone (0 warnings on 1.97.1, the pinned
|
|
# toolchain), so a warning here is now new breakage rather than old noise.
|
|
#
|
|
# This only means anything because src-tauri/rust-toolchain.toml pins the
|
|
# compiler: clippy's lint set moves between releases, so an unpinned gate
|
|
# would fail on whatever the runner happened to install. The pin and this
|
|
# flag stand or fall together — if you unpin, drop this back to advisory.
|
|
- name: Run clippy
|
|
run: |
|
|
cd src-tauri
|
|
cargo clippy --all-targets -- -D warnings
|
|
|
|
- name: Run Rust tests
|
|
run: |
|
|
cd src-tauri
|
|
cargo test
|
|
cd ..
|
|
|
|
# Fast per-commit Android compile check. This does NOT build a shippable APK:
|
|
# the full signed release APK is built only on tag pushes by build-release.yml
|
|
# (which runs sync-android-sources.sh + signing). Running the full bundle here
|
|
# too would duplicate a ~15min build and, without the sync step, produced an
|
|
# unsigned APK missing our custom sources/icons/proguard rules anyway.
|
|
# `cargo check` for the Android target (~1min) catches Android-specific Rust
|
|
# breakage without linking, bundling, or signing.
|
|
android-check:
|
|
name: Android Compile Check
|
|
runs-on: linux/amd64
|
|
needs: test
|
|
container:
|
|
image: gitea.tourolle.paris/dtourolle/jellytau-builder:2026.08.1
|
|
env:
|
|
ANDROID_HOME: /opt/android-sdk
|
|
ANDROID_SDK_ROOT: /opt/android-sdk
|
|
NDK_HOME: /opt/android-sdk/ndk/27.0.11902837
|
|
ANDROID_NDK_HOME: /opt/android-sdk/ndk/27.0.11902837
|
|
|
|
steps:
|
|
- name: Checkout repository
|
|
uses: actions/checkout@v4
|
|
|
|
- name: Cache Rust dependencies
|
|
uses: actions/cache@v3
|
|
with:
|
|
# Registry only -- never src-tauri/target. That directory is ~16 GB and
|
|
# was cached under five separate keys, which filled the runner's 74 GB
|
|
# disk at ~1.15 GB/day (23 GB in 20 days, measured Aug 2026).
|
|
# registry/src is omitted too: cargo re-extracts it for free from
|
|
# registry/cache (155 MB of .crate tarballs vs 1.1 GB extracted).
|
|
path: |
|
|
~/.cargo/registry/index
|
|
~/.cargo/registry/cache
|
|
~/.cargo/git/db
|
|
# One shared key across every job. The old per-job keys existed to stop
|
|
# debug/release target artifacts clobbering each other; with target no
|
|
# longer cached, registry contents are target-independent, so all jobs
|
|
# want the same crates. First job to finish saves; the rest restore.
|
|
key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
|
|
restore-keys: |
|
|
${{ runner.os }}-cargo-registry-
|
|
|
|
- name: Cache Node dependencies
|
|
uses: actions/cache@v3
|
|
with:
|
|
path: |
|
|
~/.bun/install/cache
|
|
node_modules
|
|
key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock') }}
|
|
restore-keys: |
|
|
${{ runner.os }}-bun-
|
|
|
|
- name: Install dependencies
|
|
run: bun install
|
|
|
|
- name: Cargo check (aarch64-linux-android)
|
|
run: |
|
|
TC="$NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin"
|
|
export CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER="$TC/aarch64-linux-android24-clang"
|
|
export CC_aarch64_linux_android="$TC/aarch64-linux-android24-clang"
|
|
export AR_aarch64_linux_android="$TC/llvm-ar"
|
|
cd src-tauri
|
|
cargo check --target aarch64-linux-android --lib
|
|
|
|
# Supply-chain gate. Until this job existed the project had no vulnerability
|
|
# scanning of any kind: nothing checked the ~500-crate Rust graph or the JS
|
|
# dependencies against a CVE feed, and nothing checked that everything we
|
|
# redistribute is licence-compatible with shipping JellyTau under MIT.
|
|
#
|
|
# The first run of this found eight vulnerabilities and one unsoundness
|
|
# (bytes, four in rustls-webpki, time, two in quick-xml, rand) — all fixed by
|
|
# `cargo update`, none of which anybody had reason to run.
|
|
#
|
|
# Runs in parallel with android-check rather than after `test`: a dependency
|
|
# advisory has nothing to do with whether the tests pass, and finding out
|
|
# sooner is the point.
|
|
security:
|
|
name: Supply Chain
|
|
runs-on: linux/amd64
|
|
container:
|
|
image: gitea.tourolle.paris/dtourolle/jellytau-builder:2026.08.1
|
|
|
|
steps:
|
|
- name: Checkout repository
|
|
uses: actions/checkout@v4
|
|
|
|
- name: Cache Rust dependencies
|
|
uses: actions/cache@v3
|
|
with:
|
|
path: |
|
|
~/.cargo/registry/index
|
|
~/.cargo/registry/cache
|
|
~/.cargo/git/db
|
|
key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
|
|
restore-keys: |
|
|
${{ runner.os }}-cargo-registry-
|
|
|
|
# cargo-deny is baked into the builder image. It fetches the RustSec
|
|
# advisory database at run time — that is *data*, like the crates
|
|
# `bun install` fetches, not a toolchain install, so the 🔴 rule in
|
|
# CLAUDE.md is not in play here.
|
|
#
|
|
# Config and every documented exception live in src-tauri/deny.toml.
|
|
# Vulnerabilities and unsoundness are hard failures with no override;
|
|
# unmaintained transitive crates that have no safe upgrade (Tauri's GTK3
|
|
# stack, the unic-* tables) are ignored there by ID, each with a reason.
|
|
- name: cargo-deny (advisories, licences, bans, sources)
|
|
run: |
|
|
cd src-tauri
|
|
cargo deny check
|
|
|
|
# Advisory for now, deliberately. The Rust graph was clean after one
|
|
# update pass, so gating it costs nothing; the JS graph has not been
|
|
# audited before and a first run that fails the build teaches everyone to
|
|
# ignore this job. Promote to a hard gate once the output is empty and
|
|
# stays empty — same approach that got clippy from advisory to -D warnings.
|
|
- name: bun audit (advisory)
|
|
run: |
|
|
bun install
|
|
bun audit || echo "::warning::bun audit reported findings — advisory for now, see CLAUDE.md"
|