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

356 lines
15 KiB
Markdown

# 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.
```bash
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:
```bash
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):
```json
{
"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:
```bash
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:
```json
{
"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 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.
```bash
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
```bash
cd crates/tauri-app
npm install @tauri-apps/plugin-updater @tauri-apps/plugin-process
```
```ts
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
```rust
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.