libmpv on Windows (DR-237): - The builder image carries zhongfly's LGPL libmpv-2.dll, pinned by asset name and sha256, plus an MSVC mpv.lib generated from the DLL's own mpv_* exports (the archive ships only a MinGW .dll.a). LGPL, not the GPL builds: no x264/x265, mpv -Dgpl=false; FFmpeg is version3, so LGPL-3.0. THIRD_PARTY_NOTICES.md records it. - build-windows-cross.sh stages both files into src-tauri/windows-libs/; build.rs links mpv.lib from there and tauri.windows.conf.json bundles the DLL beside jellytau.exe from there, with the licence texts under licenses/. One directory, so the DLL shipped is the one linked. - Workflows move to builder image 2026.09.1. Windows audio: - MpvBackend replaces WebviewAudioBackend on Windows (ao=wasapi), so volume, EQ, normalization and gapless work there as on Linux. Local files are passed to mpv as native paths, not file:// URLs. Fixes found on the way: - player_play_item decided "does the backend render video" with cfg!(not(linux)), true on Windows, while get_player_status sent Windows video to the <video> element. With mpv as the backend that would decode every film's soundtrack twice. All three callers now ask video_renders_natively() (UT-273). - confine_queued_path rebuilt paths with PathBuf::push, so on Windows a queued `downloads/x` was stored as `downloads\x`, no longer the spelling the app built. It now keeps the caller's separator (UT-205, which only ever ran on Linux, failed under Windows). Verified: jellytau.exe imports libmpv-2.dll; the full unit suite cross-compiled for Windows passes under wine against the shipped DLL (943/943, including the mpv injection and TLS tests); the NSIS installer contains the DLL and licence texts. Not yet run on real Windows hardware.
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.09.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.09.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.09.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"
|