Compare commits

...
Author SHA1 Message Date
dtourolle 9c75e74ea3 fix(ci): give the builder image what linuxdeploy needs for the AppImage
🏗️ 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
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
dtourolle 76a2d9609b fix(release): produce updater artifacts, and point the manifest at them
🏗️ Build and Test JellyTau / Run Tests (push) Successful in 15m32s
🏗️ Build and Test JellyTau / Supply Chain (push) Failing after 29s
Publish Documentation / Build & publish docs to gitea-pages (push) Successful in 5m34s
Traceability Validation / Check Requirement Traces (push) Successful in 11s
Build & Release / Run Tests (push) Successful in 14m49s
🏗️ Build and Test JellyTau / Android Compile Check (push) Successful in 4m22s
Build & Release / Build Linux (push) Failing after 17m42s
Build & Release / Build Windows (push) Successful in 15m46s
Build & Release / Build Android (push) Successful in 30m54s
Build & Release / Create Release (push) Skipped
Two defects on the release path, both of which would have failed the
v0.10.0 build after all three platforms had already compiled -- caught by
running a real signed build locally instead of waiting for the tag.

**createUpdaterArtifacts was never set.** Without it Tauri emits only the
plain .AppImage and .exe: no signatures at all. The manifest step then
finds none and aborts by design, so the release dies at Create Release
having spent ~40 minutes building artifacts it cannot publish.

**The manifest looked for the wrong filename.** Tauri v2 signs the
.AppImage *itself* and writes <name>.AppImage.sig beside it. The
.AppImage.tar.gz form this workflow globbed for only exists under
createUpdaterArtifacts: "v1Compatible". A real signed build produced:

  154M JellyTau_0.10.0_amd64.AppImage
  420  JellyTau_0.10.0_amd64.AppImage.sig

so the glob would have matched nothing and the step would have aborted
for a second, entirely different reason. Both the artifact collection and
the manifest now use the v2 names, and the AppImage and its .sig ship
together -- a manifest referencing a signature that was never uploaded
fails only on the user's machine.

Verified before tagging rather than after: the manifest logic was run
against the real artifacts (420-char minisign signature read correctly)
and the resulting latest.json checked for validity and shape.

The Windows side already used the correct pattern (<installer>.exe.sig),
which is why only Linux needed the change.
2026-08-21 23:14:16 +02:00
dtourolle 30a9cb32f5 chore(release): v0.10.0
Publish Documentation / Build & publish docs to gitea-pages (push) Canceled after 0s
🏗️ Build and Test JellyTau / Run Tests (push) Skipped
🏗️ Build and Test JellyTau / Android Compile Check (push) Skipped
🏗️ Build and Test JellyTau / Supply Chain (push) Failing after 27s
Traceability Validation / Check Requirement Traces (push) Successful in 12s
Build & Release / Run Tests (push) Successful in 18m0s
Build & Release / Build Linux (push) Failing after 17m37s
Build & Release / Build Windows (push) Successful in 15m36s
Build & Release / Build Android (push) Successful in 31m6s
Build & Release / Create Release (push) Skipped
Two user-visible features -- the app can update itself, and it can hand
you a redacted diagnostics bundle -- plus the supply-chain, release
integrity and build work behind them.

A minor bump rather than a patch, matching how v0.9.0 was cut off v0.8.2
for a single new user requirement. This one carries two (UR-077,
UR-078), both with UI in Settings.

The CHANGELOG entry is the release body now: build-release.yml publishes
the `## v0.10.0` section and fails if it is missing, instead of the fixed
block of install instructions that every release from v0.0.1 to v0.9.1
carried verbatim.
2026-08-21 22:32:04 +02:00
dtourolle 88260ab6c9 Merge pull request 'Fix release notes and stop shipping stale installers' (#16) from fix/release-artifacts-and-notes into master
Publish Documentation / Build & publish docs to gitea-pages (push) Canceled after 0s
🏗️ Build and Test JellyTau / Run Tests (push) Successful in 15m38s
🏗️ Build and Test JellyTau / Supply Chain (push) Failing after 29s
Traceability Validation / Check Requirement Traces (push) Successful in 11s
🏗️ Build and Test JellyTau / Android Compile Check (push) Successful in 4m17s
2026-08-21 20:30:54 +00:00
12 changed files with 163 additions and 20 deletions
+3 -3
View File
@@ -28,7 +28,7 @@ jobs:
if: "!startsWith(github.event.head_commit.message, 'chore(release)')" if: "!startsWith(github.event.head_commit.message, 'chore(release)')"
runs-on: linux/amd64 runs-on: linux/amd64
container: container:
image: gitea.tourolle.paris/dtourolle/jellytau-builder:2026.08 image: gitea.tourolle.paris/dtourolle/jellytau-builder:2026.08.1
steps: steps:
- name: Checkout repository - name: Checkout repository
@@ -187,7 +187,7 @@ jobs:
runs-on: linux/amd64 runs-on: linux/amd64
needs: test needs: test
container: container:
image: gitea.tourolle.paris/dtourolle/jellytau-builder:2026.08 image: gitea.tourolle.paris/dtourolle/jellytau-builder:2026.08.1
env: env:
ANDROID_HOME: /opt/android-sdk ANDROID_HOME: /opt/android-sdk
ANDROID_SDK_ROOT: /opt/android-sdk ANDROID_SDK_ROOT: /opt/android-sdk
@@ -256,7 +256,7 @@ jobs:
name: Supply Chain name: Supply Chain
runs-on: linux/amd64 runs-on: linux/amd64
container: container:
image: gitea.tourolle.paris/dtourolle/jellytau-builder:2026.08 image: gitea.tourolle.paris/dtourolle/jellytau-builder:2026.08.1
steps: steps:
- name: Checkout repository - name: Checkout repository
+18 -10
View File
@@ -21,7 +21,7 @@ jobs:
name: Run Tests name: Run Tests
runs-on: linux/amd64 runs-on: linux/amd64
container: container:
image: gitea.tourolle.paris/dtourolle/jellytau-builder:2026.08 image: gitea.tourolle.paris/dtourolle/jellytau-builder:2026.08.1
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v4
@@ -94,7 +94,7 @@ jobs:
runs-on: linux/amd64 runs-on: linux/amd64
needs: test needs: test
container: container:
image: gitea.tourolle.paris/dtourolle/jellytau-builder:2026.08 image: gitea.tourolle.paris/dtourolle/jellytau-builder:2026.08.1
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v4
@@ -190,12 +190,15 @@ jobs:
# Without nullglob an unmatched pattern stays literal, so test each # Without nullglob an unmatched pattern stays literal, so test each
# candidate instead. Same POSIX-only rule as traceability-check.yml. # candidate instead. Same POSIX-only rule as traceability-check.yml.
# #
# The .AppImage.tar.gz + .sig pair is what the updater downloads and # Tauri v2 signs the .AppImage ITSELF and writes <name>.AppImage.sig
# verifies; the plain .AppImage is what a human downloads. Both ship. # beside it -- there is no .AppImage.tar.gz unless
# bundle.createUpdaterArtifacts is set to "v1Compatible". The updater
# downloads the same AppImage a human does and verifies that .sig, so
# both files must ship or the manifest points at a signature nobody
# can fetch.
for bundle in \ for bundle in \
src-tauri/target/release/bundle/appimage/*.AppImage \ src-tauri/target/release/bundle/appimage/*.AppImage \
src-tauri/target/release/bundle/appimage/*.AppImage.tar.gz \ src-tauri/target/release/bundle/appimage/*.AppImage.sig \
src-tauri/target/release/bundle/appimage/*.AppImage.tar.gz.sig \
src-tauri/target/release/bundle/deb/*.deb \ src-tauri/target/release/bundle/deb/*.deb \
src-tauri/target/release/bundle/rpm/*.rpm; do src-tauri/target/release/bundle/rpm/*.rpm; do
[ -e "$bundle" ] || continue [ -e "$bundle" ] || continue
@@ -232,7 +235,7 @@ jobs:
# baked into the builder image. No toolchain installs here — the image has # baked into the builder image. No toolchain installs here — the image has
# cargo-xwin, clang/clang-cl, lld, llvm, nsis and the msvc target. # cargo-xwin, clang/clang-cl, lld, llvm, nsis and the msvc target.
container: container:
image: gitea.tourolle.paris/dtourolle/jellytau-builder:2026.08 image: gitea.tourolle.paris/dtourolle/jellytau-builder:2026.08.1
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v4
@@ -305,7 +308,7 @@ jobs:
runs-on: linux/amd64 runs-on: linux/amd64
needs: test needs: test
container: container:
image: gitea.tourolle.paris/dtourolle/jellytau-builder:2026.08 image: gitea.tourolle.paris/dtourolle/jellytau-builder:2026.08.1
env: env:
ANDROID_HOME: /opt/android-sdk ANDROID_HOME: /opt/android-sdk
ANDROID_SDK_ROOT: /opt/android-sdk ANDROID_SDK_ROOT: /opt/android-sdk
@@ -408,7 +411,7 @@ jobs:
needs: [build-linux, build-windows, build-android] needs: [build-linux, build-windows, build-android]
if: startsWith(github.ref, 'refs/tags/v') if: startsWith(github.ref, 'refs/tags/v')
container: container:
image: gitea.tourolle.paris/dtourolle/jellytau-builder:2026.08 image: gitea.tourolle.paris/dtourolle/jellytau-builder:2026.08.1
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v4
@@ -499,8 +502,13 @@ jobs:
APPIMAGE_URL="" APPIMAGE_URL=""
NSIS_URL="" NSIS_URL=""
for f in artifacts/linux/*.AppImage.tar.gz; do # Tauri v2 signs the AppImage itself; <name>.AppImage.sig sits beside
# it. Verified against a real signed build before tagging -- the
# v1-style .AppImage.tar.gz is never produced with
# createUpdaterArtifacts: true.
for f in artifacts/linux/*.AppImage; do
[ -e "$f" ] || continue [ -e "$f" ] || continue
case "$f" in *.sig) continue;; esac
APPIMAGE_URL="${BASE}/$(basename "$f")" APPIMAGE_URL="${BASE}/$(basename "$f")"
[ -e "$f.sig" ] && APPIMAGE_SIG="$(cat "$f.sig")" [ -e "$f.sig" ] && APPIMAGE_SIG="$(cat "$f.sig")"
done done
+1 -1
View File
@@ -21,7 +21,7 @@ jobs:
name: Build & publish docs to gitea-pages name: Build & publish docs to gitea-pages
runs-on: linux/amd64 runs-on: linux/amd64
container: container:
image: gitea.tourolle.paris/dtourolle/jellytau-builder:2026.08 image: gitea.tourolle.paris/dtourolle/jellytau-builder:2026.08.1
steps: steps:
- name: Checkout code - name: Checkout code
+1 -1
View File
@@ -17,7 +17,7 @@ jobs:
runs-on: linux/amd64 runs-on: linux/amd64
name: Check Requirement Traces name: Check Requirement Traces
container: container:
image: gitea.tourolle.paris/dtourolle/jellytau-builder:2026.08 image: gitea.tourolle.paris/dtourolle/jellytau-builder:2026.08.1
steps: steps:
- name: Checkout repository - name: Checkout repository
+99
View File
@@ -9,6 +9,105 @@ generated trace matrix lives in [docs/traceability.md](docs/traceability.md).
For how long each fixed defect had been shipping before it was found, see For how long each fixed defect had been shipping before it was found, see
[docs/defect-windows.md](docs/defect-windows.md). [docs/defect-windows.md](docs/defect-windows.md).
## v0.10.0
Two things you can see, and a great deal of work on how this project builds and
ships itself. The app can now update itself, and it can tell you what it did
when something goes wrong — both of which existed as gaps rather than as bugs,
which is why they lasted so long.
### ✨ Changes
- **JellyTau can update itself.** Anyone who installed an AppImage or ran the
Windows installer was frozen on that version permanently: nothing in the app
ever mentioned that a newer one existed, and the release page was the only
announcement. Settings → Updates now checks, shows what changed, and installs
and restarts on request. Each download is verified against JellyTau's signing
key before anything is installed, so a substituted file is refused rather than
run. Android is deliberately not wired to this — an app may not replace its own
APK, that is the system installer's job — and is given a link to the releases
page instead of a button that would fail. (UR-077 → DR-217)
- **You can export a diagnostics bundle.** Until now the app forgot everything it
had done the moment it closed. Logs went to standard output, which nobody sees
when launching from a desktop icon, and on Android went nowhere at all — so the
backend was invisible on the platform where the hardest playback bugs live. A
crash left nothing behind. Logs are now kept in a size-capped file that
survives a restart, a crash is recorded before the app dies, and Settings →
Diagnostics exports the lot as one file to attach to a bug report. Access
tokens and passwords are stripped before anything is written to disk, not
merely before it is exported. Nothing is transmitted anywhere; you attach the
file yourself. (UR-078 → DR-218)
- **Linux gets an AppImage again.** The release notes have advertised one for
months while the build never produced it — the packaging step looked for the
file, found nothing, and said nothing. (DR-217)
### 🐛 Fixes
- **Releases no longer ship every Windows installer ever built.** Every release
from v0.1.0 to v0.8.2 carried its predecessors': sixteen installers on v0.8.2,
thirteen of them stale, and a download list on v0.5.0 reaching back to 0.1.0.
The build directory is never cleaned and the build machine reuses it, so each
release collected whatever was left behind. It went unnoticed for eight months
because nothing looked wrong — the files were real and the page merely looked
busy. The stale files have been removed from the published releases, the build
now clears that directory first, and a check refuses to publish a release
containing an artifact from a different version. (DR-220)
- **Release notes now say what changed.** All 35 previous releases published the
same block of generic install instructions, whose "What's New" section was a
link to a file that does not resolve from a release page. Every release page
now carries its own entry from this changelog, and the past ones have been
filled in. (DR-219)
### 🔒 Security and supply chain
- **Dependencies are now checked against a vulnerability database on every
build.** They never had been. The first run found eight vulnerabilities and one
unsoundness in the Rust dependency graph — all of them fixed by an update
nobody had a reason to run. Licences are checked against an allow-list too, so
nothing gets redistributed inside a release that does not permit it.
(DR-216)
- **Every release publishes checksums and a bill of materials.** `SHA256SUMS`
lets you verify a download (`sha256sum -c SHA256SUMS`); the SBOM lists what
went into the build, so "does this release contain <vulnerable library>?" has
an answer that is not "rebuild it and find out". (DR-216)
- **Builds are reproducible again.** Every CI job named a container image tag
that was rewritten in place, so rebuilding an old release did not necessarily
rebuild the same thing. Jobs now pin an immutable tag. The one dependency that
comes from a git branch rather than a package registry is pinned to an exact
revision, closing a path by which new upstream code could arrive unreviewed in
a library linked into the player. (DR-216)
### 🧹 Under the hood
- Formatting, linting and type-checking now run in CI. All three were configured
and enforced by nothing: 199 files did not match the project's own formatter, a
type error could sit on the main branch until somebody cut a release, and the
test-coverage command had been broken for months by a dependency mismatch.
Coverage now has a floor that only moves up. (DR-215)
- The traceability matrix counts requirements implemented by configuration.
Several carried the necessary annotations and were being counted as uncovered
because the extraction tool only read source files. (DR-215)
- The project now has a security policy, contribution guide, code of conduct,
issue and pull-request templates, and an operations document covering the
builder image, the release secrets, and what losing the signing key would mean.
- The app framework moved from Tauri 2.9.5 to 2.11.5. Nothing about this is
visible in use, but it is worth recording that it did not go quietly: the
windowing layer beneath Tauri quietly stopped publishing the Android JavaVM
and application handle that this app's credential storage had been reading for
its whole life. Nothing here had changed; a side effect several dependencies
down had simply gone away, and the app aborted on launch on every Android
device. JellyTau now sets that handle itself rather than relying on someone
else to do it. Caught by installing on a real tablet before release — no test
suite runs the app. (UR-012 → DR-223)
## v0.9.1 ## v0.9.1
A one-line fix to the home screen, released on its own because it is the kind of A one-line fix to the home screen, released on its own because it is the kind of
+11
View File
@@ -141,6 +141,17 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
lld \ lld \
llvm \ llvm \
nsis \ nsis \
# AppImage bundling. linuxdeploy embeds xdg-open into the AppImage and
# aborts the whole bundle if it is missing:
# failed to bundle project: xdg-open binary not found
# It is present on most desktop distros, which is why the AppImage built on
# a developer machine and failed here. desktop-file-utils and zsync are the
# other two linuxdeploy commonly wants (desktop-file-validate, and zsync for
# delta updates), added together so a missing one does not cost another
# image rebuild and another failed release build.
xdg-utils \
desktop-file-utils \
zsync \
&& rm -rf /var/lib/apt/lists/* \ && rm -rf /var/lib/apt/lists/* \
# Ubuntu's clang package ships clang but NOT the clang-cl alias that cc-rs # Ubuntu's clang package ships clang but NOT the clang-cl alias that cc-rs
# invokes for MSVC targets. clang-cl is the same binary in MSVC-compat mode, # invokes for MSVC targets. clang-cl is the same binary in MSVC-compat mode,
+24
View File
@@ -61,6 +61,11 @@ filled. Keep a couple of dated tags live and prune the rest.
The order matters — CI breaks if the workflow lands before the image exists. The order matters — CI breaks if the workflow lands before the image exists.
A caveat learned the hard way: the *trailing* layer is only fast for `cargo
install` tools. Adding an **apt** package invalidates the packaging layer, which
sits above the `cargo-xwin`/`cargo-deny` installs, so those recompile too — a
~20 minute rebuild rather than ~2.
```bash ```bash
# 1. Edit Dockerfile.builder. Put new tools in the TRAILING layer: it exists so # 1. Edit Dockerfile.builder. Put new tools in the TRAILING layer: it exists so
# a tool change is a ~2 min rebuild instead of ~15. # a tool change is a ~2 min rebuild instead of ~15.
@@ -103,6 +108,25 @@ transitive upgrade (bumping `tauri-plugin-log` to 2.9.0 also moved `wry`,
therefore video playback. That is a change to make deliberately, with a full therefore video playback. That is a change to make deliberately, with a full
build and a playback check — not one to slip into a release. build and a playback check — not one to slip into a release.
## AppImage needs more than the Rust toolchain
`linuxdeploy` (which Tauri downloads at build time to assemble the AppImage)
shells out to distro tools that a minimal server image does not have. It aborts
the whole bundle on the first one missing:
```
failed to bundle project: xdg-open binary not found
```
The image therefore carries `xdg-utils`, `desktop-file-utils` and `zsync`. This
is a class of failure that **cannot be caught by building locally**: a developer
machine is a desktop and has all three, so the AppImage builds there and fails in
CI. It cost one release build to find.
Tauri's AppImage bundler also downloads `linuxdeploy`, `AppRun` and two plugin
scripts from GitHub during the build. That is Tauri's behaviour, not ours, but it
means an AppImage build depends on GitHub being reachable from the runner.
## Secrets ## Secrets
Managed with the `tea` CLI (`tea actions secrets list`) or the repo settings UI. Managed with the `tea` CLI (`tea actions secrets list`) or the repo settings UI.
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "jellytau", "name": "jellytau",
"version": "0.9.1", "version": "0.10.0",
"description": "A cross-platform Jellyfin client built with Tauri, SvelteKit and Rust.", "description": "A cross-platform Jellyfin client built with Tauri, SvelteKit and Rust.",
"author": "Duncan Tourolle <duncan@tourolle.paris>", "author": "Duncan Tourolle <duncan@tourolle.paris>",
"license": "MIT", "license": "MIT",
+1 -1
View File
@@ -8,7 +8,7 @@
# tarball/VCS URL and drop the local-copy prepare() step. # tarball/VCS URL and drop the local-copy prepare() step.
pkgname=jellytau pkgname=jellytau
pkgver=0.9.1 pkgver=0.10.0
pkgrel=1 pkgrel=1
pkgdesc="A cross-platform Jellyfin client" pkgdesc="A cross-platform Jellyfin client"
arch=('x86_64') arch=('x86_64')
+1 -1
View File
@@ -2181,7 +2181,7 @@ dependencies = [
[[package]] [[package]]
name = "jellytau" name = "jellytau"
version = "0.9.1" version = "0.10.0"
dependencies = [ dependencies = [
"aes-gcm", "aes-gcm",
"async-trait", "async-trait",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "jellytau" name = "jellytau"
version = "0.9.1" version = "0.10.0"
description = "A cross-platform Jellyfin client" description = "A cross-platform Jellyfin client"
authors = ["Duncan Tourolle <duncan@tourolle.paris>"] authors = ["Duncan Tourolle <duncan@tourolle.paris>"]
license = "MIT" license = "MIT"
+2 -1
View File
@@ -1,7 +1,7 @@
{ {
"$schema": "https://schema.tauri.app/config/2", "$schema": "https://schema.tauri.app/config/2",
"productName": "JellyTau", "productName": "JellyTau",
"version": "0.9.1", "version": "0.10.0",
"identifier": "com.dtourolle.jellytau", "identifier": "com.dtourolle.jellytau",
"build": { "build": {
"beforeDevCommand": "bun run dev", "beforeDevCommand": "bun run dev",
@@ -44,6 +44,7 @@
}, },
"bundle": { "bundle": {
"active": true, "active": true,
"createUpdaterArtifacts": true,
"targets": [ "targets": [
"deb", "deb",
"rpm", "rpm",