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 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. - 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=159 - 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 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 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"