Files
tomdeboneandClaude Opus 5 beab66648c ci: Release-Workflow für den Desktop-Client
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
2026-09-10 22:58:25 +02:00

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:defaultallow-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 --versiontauri-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 durch darwin-aarch64.
  • {{arch}} liefert die Architektur (x86_64, aarch64, i686, armv7, riscv64).
  • {{bundle_type}} liefert den laufenden Bundle-Typ: app, msi, nsis, appimage, deb oder rpm — 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}. oslinux | darwin | windows, archi686 | x86_64 | armv7 | aarch64 | riscv64, installerappimage | deb | rpm | app | msi | nsis. Die Kurzform darwin-aarch64 reicht also; die lange Form windows-x86_64-nsis ist nur nötig, wenn man MSI und NSIS gleichzeitig ausliefert.
  • version wird als Semver geparst. Ein Update wird nur angeboten, wenn release.version > current_version — die Version kommt aus dem Feld version in tauri.conf.json (heute 0.1.0). Ein Release ohne Versionsbump wird stillschweigend ignoriert.
  • signature ist der Inhalt der zugehörigen .sig-Datei, nicht deren URL.
  • notes und pub_date sind optional; pub_date als RFC 3339.
  • Ein dynamischer Endpoint darf mit 204 No Content antworten, wenn es kein Update gibt. Dann liefert er statt der platforms-Map ein flaches Objekt mit version, url und signature fü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

  1. version in src-tauri/tauri.conf.json erhöhen (Semver; der Vergleich entscheidet, ob ein Update angeboten wird). package.json und src-tauri/Cargo.toml der Konsistenz halber mitziehen.
  2. Auf jedem Plattform-Host mit gesetzten TAURI_SIGNING_PRIVATE_KEY*-Variablen und dem Release-Overlay bauen.
  3. Installer und Updater-Artefakte einsammeln, .sig-Inhalte auslesen.
  4. latest.json schreiben und zusammen mit den Artefakten unter einer https-URL veröffentlichen. Die URL muss zum endpoints-Eintrag passen.
  5. 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/.app werden 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.json erweitert 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 Nachbarprojekt lserver waren 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.json erzeugt (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 (bundle enthält weder macOS.signingIdentity noch windows.certificateThumbprint). Der Tauri-Updater-Schlüssel ersetzt das nicht — ohne Notarisierung meldet Gatekeeper die App als nicht verifiziert.
  • Die Felder active und dialog in plugins.updater sind v1-Reste ohne Wirkung (siehe oben) und sollten beim Scharfschalten entfernt werden.