Files
jellytau/.gitea/workflows/build-and-test.yml
T
dtourolle 9c75e74ea3
🏗️ Build and Test JellyTau / Run Tests (push) Successful in 15m52s
🏗️ Build and Test JellyTau / Supply Chain (push) Failing after 29s
Publish Documentation / Build & publish docs to gitea-pages (push) Successful in 5m35s
Traceability Validation / Check Requirement Traces (push) Successful in 11s
Build & Release / Run Tests (push) Successful in 14m53s
🏗️ Build and Test JellyTau / Android Compile Check (push) Successful in 4m22s
Build & Release / Build Linux (push) Successful in 20m53s
Build & Release / Build Windows (push) Successful in 15m41s
Build & Release / Build Android (push) Successful in 30m46s
Build & Release / Create Release (push) Successful in 38s
fix(ci): give the builder image what linuxdeploy needs for the AppImage
The v0.10.0 release build failed in Build Linux after 16 minutes:

  failed to bundle project: xdg-open binary not found
  /usr/bin/xdg-open: No such file or directory

linuxdeploy embeds xdg-open into the AppImage and aborts the whole bundle
when it is absent. deb and rpm had already bundled fine; only AppImage
was affected.

This is the one failure tonight that building locally could not have
caught, and the reason is worth writing down: a developer machine is a
desktop and always has xdg-utils, so the AppImage builds there and fails
on a minimal server image. The asymmetry is the bug. Every other release
defect this evening was found by building locally first; this one needed
the runner.

xdg-utils, desktop-file-utils and zsync are added together rather than
one at a time. Each round trip costs an image rebuild plus a failed
release build, and those three are what linuxdeploy commonly reaches for
(xdg-open, desktop-file-validate, and zsync for delta updates).

Workflows move to jellytau-builder:2026.08.1, built and pushed with all
three verified present inside it before this commit.

ci-operations.md gains two things learned here: that an apt addition
invalidates the layer above the cargo-install steps, so it is a ~20 minute
rebuild rather than the ~2 minutes the trailing layer normally gives; and
that Tauri's AppImage bundler downloads linuxdeploy, AppRun and two plugin
scripts from GitHub during the build, so an AppImage build depends on
GitHub being reachable from the runner.
2026-08-22 02:52:32 +02:00

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