Das Repo hatte keine CI. Der Workflow folgt dem Muster, das im Nachbarprojekt lserver produktiv läuft: Tag v*.*.* baut, lädt die Bundles als Artefakte hoch und hängt sie an ein Gitea-Release. Zwei Jobs. Windows läuft auf dem Runner-Label `windows` (VM 131 winbuild) und liefert MSI und NSIS. Linux läuft bewusst NICHT auf `ubuntu-latest`: auf diesem Runner ist das Label auf docker://node:22-bookworm gemappt, also einen Container mit Node, aber ohne Rust und ohne GTK — der Job wäre bei cargo abgebrochen. Nachgesehen in /var/lib/gitea-runner/.runner. Das Host-Label heißt linux-amd64 und hat die vollständige Toolchain. macOS hat keinen Runner; der Weg für lokale .dmg-Builds steht als Kommentar im Workflow und in docs/tauri-release.md. AppImage ist ein eigener Schritt mit continue-on-error, weil es laut Infrastruktur-Doku an linuxdeploy/FUSE scheitert — deb und rpm sollen davon nicht mitgerissen werden. APPIMAGE_EXTRACT_AND_RUN und NO_STRIP sind gesetzt, patchelf war auf dem Runner nicht installiert und wurde nachgezogen. Vermerkt ist auch die Falle, die lserver einen Tag CI gekostet hat: sobald .gitea/workflows/ existiert, ignoriert Gitea .github/workflows/ vollständig — ohne roten Lauf, einfach still. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013HC9HLrUU1LNwkzp8nkDLX
15 KiB
Tauri-Release und Auto-Update
Der Desktop-Client ist auf Auto-Update vorbereitet, aber nicht scharf
geschaltet. Dieses Dokument beschreibt den vollständigen Weg von den
Signing-Keys bis zum ausgelieferten latest.json — und was dafür im Repo noch
fehlt.
Alle Pfade sind relativ zu crates/tauri-app/, sofern nicht anders angegeben.
Ist-Zustand
| Baustein | Stand |
|---|---|
| Rust-Plugin | tauri_plugin_updater::Builder::new().build() ist in src-tauri/src/lib.rs registriert |
| Capability | src-tauri/capabilities/default.json enthält updater:default → allow-check, allow-download, allow-install, allow-download-and-install für das main-Window |
| Config | src-tauri/tauri.conf.json, Block plugins.updater: leerer pubkey, Platzhalter-Endpoint https://releases.maarcadetweet.local/{{target}}/{{arch}}/{{current_version}} |
| Updater-Artefakte | bundle.createUpdaterArtifacts ist nicht gesetzt (Default: aus) — der Build erzeugt keine .sig-Dateien und kein .app.tar.gz |
| Aufruf im Client | Fehlt. Weder das npm-Paket @tauri-apps/plugin-updater noch ein check()-Aufruf existieren (weder in src/ noch in src-tauri/src/) |
| CLI | @tauri-apps/cli ist als devDependency installiert, lokal geprüft: npx tauri --version → tauri-cli 2.11.4 |
| Plugin-Version | tauri-plugin-updater 2.10.1 laut src-tauri/Cargo.lock |
Wichtig: Die Felder active und dialog in plugins.updater stammen aus
Tauri v1. Der v2-Updater kennt sie nicht — seine Config besteht aus
endpoints, pubkey, windows, dangerousInsecureTransportProtocol,
dangerousAcceptInvalidCerts, dangerousAcceptInvalidHostnames. Unbekannte
Schlüssel werden beim Deserialisieren still ignoriert. "active": false
schaltet den Updater also nicht ab; dass heute nichts passiert, liegt
allein daran, dass niemand check() aufruft. Wer den Updater wirklich
deaktivieren will, entfernt den plugins.updater-Block und die
tauri_plugin_updater-Registrierung.
1. Signing-Keys erzeugen
Der Updater akzeptiert nur signierte Artefakte. Das Schlüsselpaar wird einmal erzeugt und danach nie wieder gewechselt — ein neuer Public Key macht alle installierten Clients update-unfähig.
cd crates/tauri-app
npx tauri signer generate -w ~/.tauri/maarcadetweet.key
Optionen laut npx tauri signer generate --help:
| Flag | Bedeutung |
|---|---|
-w, --write-keys <PFAD> |
Privaten Schlüssel in eine Datei schreiben (sonst nur stdout) |
-p, --password <PW> |
Passwort für den privaten Schlüssel setzen |
-f, --force |
Vorhandene Datei überschreiben |
--ci |
Keine interaktiven Rückfragen |
Ergebnis: ~/.tauri/maarcadetweet.key (privat, niemals ins Repo) und
~/.tauri/maarcadetweet.key.pub (öffentlich, base64). Der Inhalt der
.pub-Datei ist genau das, was in pubkey gehört.
Für den Build-Rechner bzw. die CI:
export TAURI_SIGNING_PRIVATE_KEY="$(cat ~/.tauri/maarcadetweet.key)"
export TAURI_SIGNING_PRIVATE_KEY_PASSWORD="…"
TAURI_SIGNING_PRIVATE_KEY akzeptiert den Schlüsselinhalt oder einen Pfad.
Fehlt die Variable beim Build mit aktivierten Updater-Artefakten, bricht das
Bundling ab.
2. Konfiguration — ohne den Dev-Zustand zu brechen
Die laufende src-tauri/tauri.conf.json soll unangetastet bleiben (leerer
pubkey, keine Updater-Artefakte). Die CLI kann Konfigurationen zur Build-Zeit
zusammenführen:
-c, --config <CONFIG> JSON strings or paths to JSON, JSON5 or TOML files to
merge with the default configuration file
Der saubere Weg ist deshalb eine Release-Overlay-Datei, die nur die
Release-Unterschiede enthält. Anlegen als
crates/tauri-app/src-tauri/tauri.release.conf.json (existiert noch nicht):
{
"bundle": {
"createUpdaterArtifacts": true
},
"plugins": {
"updater": {
"endpoints": [
"https://releases.example.org/maarcadetweet/latest.json"
],
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6…HIER_DER_INHALT_VON_.key.pub…",
"windows": {
"installMode": "passive"
}
}
}
}
Build damit:
cd crates/tauri-app
npx tauri build --config src-tauri/tauri.release.conf.json
Feldbedeutungen (geprüft an tauri-plugin-updater 2.11 / tauri-utils 2.9):
| Feld | Bedeutung |
|---|---|
bundle.createUpdaterArtifacts |
true erzeugt die Updater-Artefakte (.sig, unter macOS zusätzlich das .app.tar.gz). Alternativ "v1Compatible" für v1-Clients |
plugins.updater.pubkey |
Public Key aus Schritt 1. Pflichtfeld — fehlt es ganz, scheitert das Deserialisieren der Plugin-Config |
plugins.updater.endpoints |
Liste von URLs, der Reihe nach probiert. Müssen https sein: in Release-Builds bricht ein http-Endpoint mit InsecureTransportProtocol ab (im Debug-Build nur eine Warnung), es sei denn dangerousInsecureTransportProtocol ist gesetzt |
plugins.updater.windows.installMode |
passive (Default), basicUi oder quiet |
Endpoint-Platzhalter
In der Endpoint-URL werden ersetzt: {{current_version}}, {{target}},
{{arch}} und {{bundle_type}}. Achtung auf einen Fallstrick — der bereits in
der Config stehende Platzhalter-Endpoint benutzt genau diese Form:
{{target}}wird in der URL nur durch das Betriebssystem ersetzt (linux,darwin,windows) — nicht durchdarwin-aarch64.{{arch}}liefert die Architektur (x86_64,aarch64,i686,armv7,riscv64).{{bundle_type}}liefert den laufenden Bundle-Typ:app,msi,nsis,appimage,deboderrpm— bzw.unknown, wenn er nicht bestimmbar ist.
Ein statisches latest.json (eine Datei für alle Plattformen) ist der
einfachere Weg und kommt ohne Platzhalter aus.
3. latest.json
Der Updater versteht zwei Formate. Für einen einfachen Static-File-Server ist das statische Format richtig:
{
"version": "0.2.0",
"notes": "Thread-Ansicht, schnellere Timeline",
"pub_date": "2026-01-15T10:00:00Z",
"platforms": {
"darwin-aarch64": {
"signature": "<Inhalt von maarcadetweet.app.tar.gz.sig>",
"url": "https://releases.example.org/maarcadetweet/0.2.0/maarcadetweet_aarch64.app.tar.gz"
},
"darwin-x86_64": {
"signature": "…",
"url": "https://releases.example.org/maarcadetweet/0.2.0/maarcadetweet_x64.app.tar.gz"
},
"linux-x86_64": {
"signature": "…",
"url": "https://releases.example.org/maarcadetweet/0.2.0/maarcadetweet_0.2.0_amd64.AppImage"
},
"windows-x86_64": {
"signature": "…",
"url": "https://releases.example.org/maarcadetweet/0.2.0/maarcadetweet_0.2.0_x64-setup.exe"
}
}
}
Regeln, die der Plugin-Code vorgibt:
- Schlüssel der
platforms-Map: Der Updater sucht in dieser Reihenfolge nach{os}-{arch}-{installer}und danach nach{os}-{arch}.os∈linux|darwin|windows,arch∈i686|x86_64|armv7|aarch64|riscv64,installer∈appimage|deb|rpm|app|msi|nsis. Die Kurzformdarwin-aarch64reicht also; die lange Formwindows-x86_64-nsisist nur nötig, wenn man MSI und NSIS gleichzeitig ausliefert. versionwird als Semver geparst. Ein Update wird nur angeboten, wennrelease.version > current_version— die Version kommt aus dem Feldversionintauri.conf.json(heute0.1.0). Ein Release ohne Versionsbump wird stillschweigend ignoriert.signatureist der Inhalt der zugehörigen.sig-Datei, nicht deren URL.notesundpub_datesind optional;pub_dateals RFC 3339.- Ein dynamischer Endpoint darf mit
204 No Contentantworten, wenn es kein Update gibt. Dann liefert er statt derplatforms-Map ein flaches Objekt mitversion,urlundsignaturefür genau die anfragende Plattform.
4. Build pro Plattform
tauri build ist ein Cross-Build nur eingeschränkt — jede Plattform wird auf
ihrem eigenen Host (oder Runner) gebaut.
cd crates/tauri-app
# macOS (Apple Silicon)
npx tauri build --config src-tauri/tauri.release.conf.json
# macOS Universal (beide Rust-Targets müssen installiert sein)
npx tauri build --target universal-apple-darwin \
--config src-tauri/tauri.release.conf.json
# Windows / Linux jeweils auf dem passenden Host, gleicher Aufruf
Ablauf laut build.beforeBuildCommand in tauri.conf.json: erst
npm run build (Vite → crates/tauri-app/dist, entspricht frontendDist: "../dist"), dann der Cargo-Release-Build, dann das Bundling.
bundle.targets steht auf "all".
Nützliche Flags (aus npx tauri build --help):
| Flag | Zweck |
|---|---|
-t, --target <TRIPLE> |
Ziel-Triple, oder universal-apple-darwin |
-b, --bundles <LISTE> |
Nur bestimmte Bundle-Typen erzeugen |
--no-bundle |
Nur das Binary, kein Installer |
-c, --config <PFAD> |
Config-Overlay (siehe oben) |
--ci |
Keine interaktiven Rückfragen |
5. Wo die Artefakte landen
crates/tauri-app/src-tauri/Cargo.toml deklariert ein eigenes [workspace],
der Build-Output liegt deshalb nicht im Repo-Root-target/, sondern unter
crates/tauri-app/src-tauri/target/release/bundle/. Mit --target <triple>
schiebt sich das Triple dazwischen:
src-tauri/target/<triple>/release/bundle/.
| Plattform | Pfad unter bundle/ |
Für den Updater relevant |
|---|---|---|
| macOS | macos/maarcadetweet.app |
macos/maarcadetweet.app.tar.gz + .tar.gz.sig |
| macOS | dmg/maarcadetweet_<version>_<arch>.dmg |
nein (nur für die Erstinstallation) |
| Windows | msi/maarcadetweet_<version>_<arch>_<lang>.msi |
.msi + .msi.sig |
| Windows | nsis/maarcadetweet_<version>_<arch>-setup.exe |
.exe + .exe.sig |
| Linux | appimage/maarcadetweet_<version>_<arch>.AppImage |
.AppImage + .AppImage.sig |
| Linux | deb/…deb, rpm/…rpm |
nein |
Die .sig-Dateien entstehen nur, wenn createUpdaterArtifacts aktiv ist und
die Signing-Env-Variablen gesetzt sind. Für das latest.json wird der
Dateiinhalt der .sig gebraucht, das Artefakt selbst wird nicht
ausgeliefert.
6. Was im Client noch fehlt
Config und Signaturen allein bewirken nichts — der Client fragt nirgends nach Updates. Es gibt zwei Wege:
a) Vom Frontend aus
cd crates/tauri-app
npm install @tauri-apps/plugin-updater @tauri-apps/plugin-process
import { check } from "@tauri-apps/plugin-updater";
import { relaunch } from "@tauri-apps/plugin-process";
const update = await check();
if (update) {
await update.downloadAndInstall();
await relaunch();
}
Die Capability erlaubt check, download, install und
downloadAndInstall bereits. Zusätzlich nötig: tauri-plugin-process als
Rust-Dependency, tauri_plugin_process::init() in lib.rs und
"process:default" in capabilities/default.json — sonst schlägt relaunch()
fehl.
b) Vom Rust-Layer aus
use tauri_plugin_updater::UpdaterExt;
if let Ok(Some(update)) = app.updater()?.check().await {
update.download_and_install(|_, _| {}, || {}).await?;
}
Ohne npm-Abhängigkeit, dafür ohne UI-Kontrolle im Frontend. Der Aufruf gehört
in den setup-Hook oder hinter ein Tauri-Command, das die Svelte-Seite
aufrufen kann.
Welcher Weg genommen wird, ist noch offen — im Repo existiert keiner von beiden.
7. Release-Checkliste
versioninsrc-tauri/tauri.conf.jsonerhöhen (Semver; der Vergleich entscheidet, ob ein Update angeboten wird).package.jsonundsrc-tauri/Cargo.tomlder Konsistenz halber mitziehen.- Auf jedem Plattform-Host mit gesetzten
TAURI_SIGNING_PRIVATE_KEY*-Variablen und dem Release-Overlay bauen. - Installer und Updater-Artefakte einsammeln,
.sig-Inhalte auslesen. latest.jsonschreiben und zusammen mit den Artefakten unter einerhttps-URL veröffentlichen. Die URL muss zumendpoints-Eintrag passen.- Mit einer älteren installierten Version gegenprüfen, dass
check()das Update findet und die Signaturprüfung durchgeht.
8. CI: Tag-Release über Gitea Actions
.gitea/workflows/release.yml baut Schritt 2 der Checkliste für Windows und
Linux automatisch. Trigger ist ein Tag v*.*.* (zusätzlich manuell per
workflow_dispatch, dann ohne Release-Anlage).
| Job | runs-on |
Runner | Bundles |
|---|---|---|---|
windows |
windows |
winbuild, 192.168.1.69 (on-demand) | msi/*.msi, nsis/*-setup.exe |
linux |
ubuntu-latest |
ci-runner, 192.168.1.72 | deb/*.deb, rpm/*.rpm, appimage/*.AppImage |
Beide Jobs laufen npm ci (es gibt eine package-lock.json) und danach den
npm-Skript-Umweg npm run tauri -- build --ci aus crates/tauri-app/, damit
die im Lock gepinnte @tauri-apps/cli benutzt wird und nicht die zufällig auf
dem Runner installierte. Die Pfade sind die aus Abschnitt 5. Beide laden ihre
Bundles als Job-Artefakt hoch und hängen sie an dasselbe Gitea-Release zum
Tag (anlegen, und falls der andere Job schneller war, das vorhandene per Tag
holen). Die Release-Beschreibung kommt aus dem passenden
## [<version>]-Abschnitt einer CHANGELOG.md, sobald es eine gibt — bis
dahin steht dort der Commit-SHA.
Zwei Dinge, die der Workflow nicht tut:
- macOS. Es gibt keinen macOS-Runner.
.dmg/.appwerden lokal nach Abschnitt 4 gebaut und im Gitea-Release von Hand angehängt. - Signierte Updater-Artefakte. Der Workflow baut ohne Release-Overlay und
ohne
TAURI_SIGNING_PRIVATE_KEY*; es entstehen also keine.sig-Dateien (Abschnitt 2 und 5). Für ein echtes Auto-Update müssen Overlay-Datei und Secrets ergänzt und der Build-Aufruf um--config src-tauri/tauri.release.conf.jsonerweitert werden.
Vor dem Tag zu bumpen (Schritt 1 der Checkliste): Die Release-Version kommt
aus dem Tag, die Version im Dateinamen aus src-tauri/tauri.conf.json. Ohne
Bump heißt das Artefakt zu v0.2.0 weiterhin
maarcadetweet_0.1.0_x64-setup.exe. src-tauri/Cargo.toml und package.json
mitziehen — alle drei stehen aktuell auf 0.1.0.
Falle: Sobald
.gitea/workflows/existiert, ignoriert Gitea.github/workflows/vollständig — kommentarlos, ohne roten Lauf. Im Nachbarprojektlserverwaren Tests dadurch einen Tag lang still abgeschaltet. Dieses Repo hat kein.github/, und das soll so bleiben: neue Workflows gehören nach.gitea/workflows/.
9. Offene Punkte
- Kein Release-Overlay im Repo — die Datei aus Abschnitt 2 muss angelegt
werden. Der Endpoint
https://releases.maarcadetweet.local/…in der aktuellen Config ist ein Platzhalter und existiert nicht. - Kein Update-Server und kein Skript, das
latest.jsonerzeugt (scripts/ist leer). Der CI-Workflow aus Abschnitt 8 baut und veröffentlicht Installer, aber keine Updater-Artefakte. - Keine Code-Signierung/Notarisierung für macOS und keine Authenticode-Signatur
für Windows konfiguriert (
bundleenthält wedermacOS.signingIdentitynochwindows.certificateThumbprint). Der Tauri-Updater-Schlüssel ersetzt das nicht — ohne Notarisierung meldet Gatekeeper die App als nicht verifiziert. - Die Felder
activeunddialoginplugins.updatersind v1-Reste ohne Wirkung (siehe oben) und sollten beim Scharfschalten entfernt werden.