Files
tessera-ctl/.planning/phases/18-desktop-client-fertigstellen/18-05-PLAN.md
T

309 lines
20 KiB
Markdown

---
phase: 18-desktop-client-fertigstellen
plan: 05
type: execute
wave: 3
depends_on: ["18-02", "18-04"]
files_modified:
- .gitea/workflows/ci.yml
- .gitea/scripts/desktop-collect.sh
- .gitea/scripts/publish-release.sh
- apps/desktop/src-tauri/Cargo.toml
- apps/desktop/src-tauri/Cargo.lock
autonomous: false
requirements: [DESK-01, DESK-04, DESK-05]
user_setup: []
estimate:
tokens: 70000
raw_tokens: 70000
tasks: 3
confidence: low
must_haves:
truths:
- "Der CI-Job desktop baut auf dem Linux-Runner zusaetzlich den Windows-Installer per Cross-Bau (cargo-xwin, NSIS) und sammelt `Tessera-Setup-X.Y.Z.exe` neben `Tessera-X.Y.Z.AppImage` ein; das Manifest traegt beide Plattformen (D-04, D-05)."
- "Ein Push auf main endet mit einem gruenen Lauf: Job desktop mit beiden Dateien, Job publish mit Abbildern, die die Pakete tragen (D-06, D-08)."
- "Jeder Fehlschlag der Pipeline wird gelesen, der Job angepasst, erneut gepusht — hoechstens drei Runden, jede als normaler Commit (D-16)."
artifacts:
- path: ".gitea/workflows/ci.yml"
provides: "Windows-Cross-Bau-Schritte im Job desktop, Einsammeln mit --require linux,windows"
contains: "cargo-xwin"
key_links:
- from: ".gitea/workflows/ci.yml (Schritt Windows NSIS Cross-Bau)"
to: ".gitea/scripts/desktop-collect.sh"
via: "Bundle-Verzeichnis target/x86_64-pc-windows-msvc/release/bundle/nsis/*.exe -> Tessera-Setup-X.Y.Z.exe"
pattern: "x86_64-pc-windows-msvc"
- from: ".gitea/workflows/ci.yml (desktop)"
to: ".gitea/workflows/ci.yml (publish)"
via: "actions/cache Schluessel desktop-dist-${{ gitea.sha }} (aus 18-02)"
pattern: "desktop-dist-"
---
<objective>
Der Windows-Installer entsteht im selben CI-Job wie das AppImage — als
Cross-Bau auf dem Linux-Runner (`cargo tauri build --runner cargo-xwin --target x86_64-pc-windows-msvc --bundles nsis`,
NSIS aus dem Ubuntu-Paket). Weil dieser Bau nur in der Pipeline beweisbar ist
(kein Windows-Werkzeug auf dem Entwicklungsrechner, siehe RESEARCH), enthaelt
der Plan die in D-16 vorgesehene Iterationsschleife: pushen, Protokoll lesen,
Job anpassen, erneut pushen — hoechstens drei Runden.
Purpose: D-04, D-05, D-06 und D-16 aus 18-CONTEXT.md; Erfolgskriterium 1
(bis auf den Release-Anhang, der erst beim naechsten Freigabe-Tag sichtbar
wird — der Upload-Pfad selbst ist in 18-02 gebaut und per Probelauf geprueft).
Output: Erweiterter Job `desktop`, gruener Pipeline-Lauf mit beiden Dateien,
Beta-Abbilder mit Paketen.
**Rollen:** Der Executor pusht nie. Der Orchestrator pusht (`git push`; die
Push-Adresse zeigt auf `localhost:3002`), beobachtet den Lauf in Gitea und
meldet Status und Protokollauszug zurueck. Der Executor liest, behebt,
committet.
</objective>
## Artifacts this phase produces
Dieser Plan: `.gitea/workflows/ci.yml` (Schritte "Windows-Werkzeuge",
"Windows-Installer bauen (Cross-Bau)", erweiterte Cache-Pfade, Einsammeln
mit `--require linux,windows`); bei Bedarf Korrekturen an
`.gitea/scripts/desktop-collect.sh`, `.gitea/scripts/publish-release.sh`
(`GITEA_API`-Umgehung) und `apps/desktop/src-tauri/Cargo.toml`
(`rustls-tls`-Ausweichlösung). Gesamtliste der Phase: siehe 18-01-PLAN.md.
<execution_context>
@$HOME/.claude/gsd-core/workflows/execute-plan.md
@$HOME/.claude/gsd-core/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/18-desktop-client-fertigstellen/18-CONTEXT.md
@.planning/phases/18-desktop-client-fertigstellen/18-RESEARCH.md
@.planning/phases/18-desktop-client-fertigstellen/18-01-SUMMARY.md
@.planning/phases/18-desktop-client-fertigstellen/18-02-SUMMARY.md
@.planning/phases/18-desktop-client-fertigstellen/18-04-SUMMARY.md
@.gitea/workflows/ci.yml
@.gitea/scripts/desktop-collect.sh
@.gitea/scripts/publish-release.sh
@apps/desktop/src-tauri/Cargo.toml
</context>
<tasks>
<task type="auto">
<name>Task 1: Windows-Cross-Bau in den Job desktop einbauen</name>
<reversibility rating="reversible">Reine Workflow-Schritte; Rueckbau ist ein Commit, kein Zustand ausserhalb des Runners ausser dem Cache.</reversibility>
<files>
.gitea/workflows/ci.yml
</files>
<read_first>
.gitea/workflows/ci.yml (Job desktop aus 18-02),
.gitea/scripts/desktop-collect.sh (Windows-Zweig: Bundle-Pfad und Zielname),
.planning/phases/18-desktop-client-fertigstellen/18-RESEARCH.md (Abschnitte "Pattern 1", "Standard Stack: Installation", "Common Pitfalls 2-5", "Open Questions 2-3"),
.planning/phases/18-desktop-client-fertigstellen/18-CONTEXT.md (Abschnitt "Specific Ideas": Runner 8 Kerne/15 GB, nacheinander im selben Job)
</read_first>
<action>
Im Job `desktop` (Datei `.gitea/workflows/ci.yml`) folgende Aenderungen,
Schrittnamen deutsch:
1. Schritt "Systemabhaengigkeiten": die apt-Liste um `lld llvm clang nsis`
erweitern (alle vier am 2026-09-16 im Runner-Abbild per `apt-cache policy`
bestaetigt: lld/llvm/clang 18, nsis 3.09).
2. Neuer Schritt "Windows-Werkzeuge" nach "Rust-Toolchain":
`rustup target add x86_64-pc-windows-msvc` und
`command -v cargo-xwin >/dev/null 2>&1 || cargo install --locked cargo-xwin`
(Legitimitaetspruefung in RESEARCH: `OK`, rust-cross/cargo-xwin, 0.23.1).
3. Schritt "Cargo-Zwischenspeicher": `path` um `~/.cargo/bin/cargo-xwin`,
`~/.cache/cargo-xwin` (Windows-SDK-Ablage von cargo-xwin, mehrere hundert
MB, soll nur einmal geladen werden) und `~/.local/share/tauri` (NSIS-Plugins,
die der Tauri-Bundler beim ersten Windows-Bau laedt) erweitern.
4. Schritt "Alte Bundles entfernen": zusaetzlich
`rm -rf apps/desktop/src-tauri/target/x86_64-pc-windows-msvc/release/bundle`.
5. Neuer Schritt "Windows-Installer bauen (Cross-Bau)" **nach** dem
AppImage-Schritt (nacheinander, ein Job, ein Cache — CONTEXT "Specific
Ideas"): `pnpm --filter @tessera/desktop exec tauri build --runner cargo-xwin --target x86_64-pc-windows-msvc --bundles nsis`.
6. Schritt "Pakete einsammeln": `--require linux,windows`.
7. Kopfkommentar des Jobs: zwei Saetze zum Cross-Bau und zum Grund, warum
die Version rein numerisch bleibt (Pitfall 2).
Keine `-j`-Begrenzung und keine `CARGO_BUILD_JOBS`-Vorgabe im ersten Anlauf;
beides ist eine Ausweichlösung der Schleife (Task 3), falls der Runner den
Speicher ausschoepft. `desktop-collect.sh` braucht keine Aenderung, wenn der
Windows-Zweig aus 18-01 (desktop-collect.sh) den Pfad
`target/x86_64-pc-windows-msvc/release/bundle/nsis/*.exe` bereits kennt —
pruefen, sonst nachziehen.
</action>
<acceptance_criteria>
- `grep -c 'cargo-xwin' .gitea/workflows/ci.yml` ergibt mindestens 3 (Install, Cache-Pfad, Bauschritt).
- `grep -c -- '--target x86_64-pc-windows-msvc --bundles nsis' .gitea/workflows/ci.yml` ergibt 1.
- `grep -c 'desktop-collect.sh --require linux,windows' .gitea/workflows/ci.yml` ergibt 1; `grep -c 'desktop-collect.sh --require linux$' .gitea/workflows/ci.yml` ergibt 0.
- Die apt-Zeile enthaelt `nsis`, `lld`, `llvm` und `clang` (`grep -E 'lld llvm clang nsis|nsis' .gitea/workflows/ci.yml`).
- `grep -c 'x86_64-pc-windows-msvc/release/bundle/nsis' .gitea/scripts/desktop-collect.sh` ergibt mindestens 1.
- `sh -n .gitea/scripts/desktop-collect.sh` endet mit 0.
</acceptance_criteria>
<verify>
<automated>cd /home/vicolab/projects/tessera-ctl && test "$(grep -c 'cargo-xwin' .gitea/workflows/ci.yml)" -ge 3 && grep -q -- '--target x86_64-pc-windows-msvc --bundles nsis' .gitea/workflows/ci.yml && grep -q 'desktop-collect.sh --require linux,windows' .gitea/workflows/ci.yml && grep -q 'nsis' .gitea/workflows/ci.yml && grep -q 'x86_64-pc-windows-msvc/release/bundle/nsis' .gitea/scripts/desktop-collect.sh && sh -n .gitea/scripts/desktop-collect.sh && node -e "const y=require('fs').readFileSync('.gitea/workflows/ci.yml','utf8');const d=y.indexOf('\n desktop:'),p=y.indexOf('\n publish:');if(d===-1||p===-1||d>p)process.exit(1);const job=y.slice(d,p);if(job.indexOf('--bundles appimage')>job.indexOf('--bundles nsis'))process.exit(2)" && echo WINDOWS-STEPS-OK</automated>
<fails_when>Ein Kennzeichen fehlt, das Einsammeln fordert Windows nicht, das Sammel-Skript kennt den NSIS-Pfad nicht, oder der Windows-Schritt steht vor dem AppImage-Schritt (Exit 2) — `WINDOWS-STEPS-OK` fehlt.</fails_when>
</verify>
<done>
Der Job `desktop` installiert Windows-Werkzeuge, baut nach dem AppImage den
NSIS-Installer per Cross-Bau, cached SDK und NSIS-Plugins und sammelt beide
Dateien ein. Commit liegt bereit fuer den Push.
</done>
</task>
<task type="checkpoint:human-action" gate="blocking">
<name>Task 2: Push und CI-Lauf beobachten — gruen mit beiden Dateien?</name>
<precondition>Der Commit aus Task 1 (bzw. aus der letzten Runde von Task 3) liegt lokal auf `main`; der Gitea-Runner `gitea-runner` laeuft (Container aktiv), das Secret `REGISTRY_TOKEN` ist gesetzt.</precondition>
<action>Den Stand nach Gitea pushen und den Pipeline-Lauf "Tessera CI/CD" beobachten — der Executor darf nicht pushen (Projektregel: Push nur durch Orchestrator/Nutzer, Push-Adresse zeigt dauerhaft auf `localhost:3002`, nie ueber `git.vicolab.de`).</action>
<instructions>
Der Executor hat den Job `desktop` um den Windows-Cross-Bau erweitert,
Skripte und Workflow statisch geprueft und committet. Was jetzt nur der
Orchestrator kann: `git push` auf `main` und den Lauf in Gitea verfolgen
(Gitea-MCP oder Weboberflaeche). Der erste Lauf dauert deutlich laenger als
bisher (Rust-Toolchain, zwei Release-Baue, Windows-SDK-Download); erst mit
warmem Cache sinkt die Zeit.
Zurueckmelden — je nach Ausgang:
**Gruen:** Aus dem Job `desktop`, Schritt "Pakete einsammeln", die beiden
Ausgabezeilen (`windows: Tessera-Setup-1.1.0-beta.{sha}.exe (…)` und
`linux: Tessera-1.1.0-beta.{sha}.AppImage (…)`), dazu Status des Jobs
`publish` (gruen) und die Zeile mit den gepushten Etiketten.
**Rot:** Name des gescheiterten Jobs und Schritts sowie die letzten rund 60
Protokollzeilen dieses Schritts (mit der eigentlichen Fehlermeldung — bei
Rust die Zeilen ab `error:` bzw. `error[E…]`, bei apt die Zeile `E:`, bei
curl den HTTP-Code und die Antwort). Diese Runde zaehlt (Runde 1 von
hoechstens 3).
</instructions>
<verification>Der Lauf ist gruen; Schritt "Pakete einsammeln" nennt genau eine `.exe` und genau ein `.AppImage`; der Job `publish` hat die Beta-Abbilder gepusht. Der Executor prueft nach der Rueckmeldung zusaetzlich `git status --porcelain` (leer) und dass `git log -1 --format=%H` dem vom Orchestrator genannten Lauf-Commit entspricht.</verification>
<resume-signal>Antworte mit "gruen" plus den beiden Dateizeilen — oder mit "rot" plus Job, Schritt und Protokollauszug.</resume-signal>
<verify>
<human-check>Der Lauf ist gruen; Schritt "Pakete einsammeln" nennt genau eine `.exe` und genau ein `.AppImage`; der Job `publish` hat die Beta-Abbilder gepusht.</human-check>
</verify>
<done>
Rueckmeldung liegt vor. Bei "gruen" ist der Plan fertig (Task 3 entfaellt).
Bei "rot" geht es mit Task 3 weiter.
</done>
</task>
<task type="auto">
<name>Task 3: Iterationsschleife — Fehler lesen, Job anpassen, erneut pushen (hoechstens drei Runden)</name>
<files>
.gitea/workflows/ci.yml,
.gitea/scripts/desktop-collect.sh,
.gitea/scripts/publish-release.sh,
apps/desktop/src-tauri/Cargo.toml,
apps/desktop/src-tauri/Cargo.lock
</files>
<read_first>
Der vom Orchestrator gelieferte Protokollauszug,
.planning/phases/18-desktop-client-fertigstellen/18-RESEARCH.md (Abschnitte "Common Pitfalls 1-5", "Open Questions", "Assumptions Log"),
.gitea/workflows/ci.yml,
.gitea/scripts/desktop-collect.sh
</read_first>
<action>
Nur ausfuehren, wenn Task 2 "rot" gemeldet hat. Je Runde: Ursache aus dem
Protokoll bestimmen, **eine** gezielte Aenderung machen, lokal pruefen
(`sh -n` fuer Skripte, `cargo check` bei Cargo-Aenderungen), committen mit
`ci(desktop): Runde N — {Ursache in fuenf Woertern}`, dann zurueck zu
Task 2 (der Orchestrator pusht und meldet). Nach der dritten roten Runde
**stoppen** und dem Nutzer den Stand mit dem letzten Protokollauszug
vorlegen (kein vierter Versuch ohne Ruecksprache).
Bekannte Fehlerbilder und die jeweils vorgesehene Aenderung (in dieser
Reihenfolge pruefen):
| Signatur im Protokoll | Ursache | Aenderung |
|---|---|---|
| `E: Unable to locate package …` | Paketname falsch/umbenannt | Namen mit `docker run --rm gitea/runner-images:ubuntu-latest sh -c 'apt-get update -qq; apt-cache policy {name}'` pruefen und in der apt-Zeile korrigieren |
| `The system library '…' required by crate '…' was not found` (pkg-config) | dev-Paket fehlt | fehlendes `lib…-dev` in die apt-Zeile aufnehmen (Pitfall 5) |
| `failed to run custom build command for openssl-sys` beim Ziel `x86_64-pc-windows-msvc` | TLS-Backend zieht OpenSSL fuer das Windows-Ziel | in `Cargo.toml` `reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }` (Pitfall 3), lokal `cargo check`, `Cargo.lock` mit committen |
| `makensis: not found` / `NSIS … not installed` | NSIS fehlt im PATH | `nsis` in der apt-Zeile pruefen; sonst Pfad `/usr/bin/makensis` per `which makensis` im Protokoll ausgeben lassen |
| `failed to download NSIS plugin` / `nsis_tauri_utils` | Netz/GitHub | gleicher Stand, erneut pushen (leerer Commit `ci(desktop): Runde N — erneuter Lauf`) |
| `llvm-rc` / `rc.exe` / `winres` / `embed-resource` | Ressourcen-Compiler nicht gefunden | `sudo ln -sf /usr/bin/llvm-rc-18 /usr/bin/llvm-rc` im Schritt "Windows-Werkzeuge" oder `env: RC: llvm-rc-18` am Bauschritt |
| `xwin` / `Failed to download` / `manifest` beim ersten Cross-Bau | Windows-SDK-Download | erneut pushen; falls wiederholt: `env: XWIN_ARCH: x86_64` und `XWIN_CACHE_DIR: ${{ github.workspace }}/.xwin-cache` (dann `.xwin-cache` in die Cache-Pfade) |
| `optional build metadata in app version must be numeric-only` | Version nicht numerisch | `git describe`-Ausgabe im Protokoll pruefen; `desktop-version.sh` haette abbrechen muessen — Regex im Skript nachziehen |
| `genau eine Datei erwartet` (Sammel-Skript) | Bundle-Pfad oder Altbestand | Pfad mit `find apps/desktop/src-tauri/target -name '*.exe' -path '*bundle*'` im Protokoll ermitteln und im Skript anpassen; Aufraeum-Schritt pruefen |
| `Cache service responded with 4xx/5xx` / `Failed to save` / `fail-on-cache-miss` obwohl gespeichert | actions/cache-Version vs. Cache-Server | `actions/cache/save@v3` und `actions/cache/restore@v3` (Open Question 3); bleibt es rot: `target` aus den Cache-Pfaden nehmen (zu gross) |
| `Killed` / `signal: 9` / `memory` waehrend `rustc` | Speicher | `env: CARGO_BUILD_JOBS: 4` am Job (CONTEXT "Specific Ideas") |
| Job-Zeitueberschreitung | Baudauer plus Cache-Sicherung | `target` aus den Cache-Pfaden nehmen, `~/.cargo/registry` und xwin-Ablage behalten |
| `docker build` scheitert an `COPY desktop-dist` | Verzeichnis fehlt im Kontext | Cache-Restore-Pfad und `test -f`-Schritt in `publish` pruefen |
| Release-Upload `413` (nur bei Tags) | Proxy-Groessengrenze vor `git.vicolab.de` | am Release-Schritt `env: GITEA_API: http://172.18.0.1:3002/api/v1` (Host-Adresse, ueber die der Runner auch seinen Cache-Server erreicht) |
| Release-Upload `400` mit "file type" (nur bei Tags) | Gitea `[repository.release] ALLOWED_TYPES` eingeschraenkt | nicht im Repository loesbar — dem Nutzer melden (Server-Einstellung); Voreinstellung der Instanz laesst alle Typen zu (geprueft 2026-09-16) |
| `cargo clippy` Fehler | Code | Stelle beheben, `cargo clippy` lokal gruen |
Jede Runde im SUMMARY festhalten: Signatur, Ursache, Aenderung, Commit.
Trifft keine Signatur zu, die Ursache aus dem Protokoll ableiten und die
kleinste plausible Aenderung waehlen; im Zweifel zuerst mehr Protokoll
anfordern (z. B. `RUST_BACKTRACE=1` oder `--verbose` am Bauschritt), statt
zu raten.
</action>
<acceptance_criteria>
- Jede Runde ist genau ein Commit mit Praefix `ci(desktop): Runde N —` (`git log --oneline -5 | grep -c 'ci(desktop): Runde'` entspricht der Rundenzahl).
- Nach jeder Aenderung: `sh -n` fuer geaenderte Skripte endet mit 0; bei Cargo-Aenderungen endet `cargo check` in `apps/desktop/src-tauri` mit 0.
- Es gibt nie mehr als drei Runden; nach der dritten roten Runde wird der Stand dem Nutzer vorgelegt statt weiter zu pushen.
- Am Ende: Task 2 hat "gruen" mit genau einer `.exe`- und einer `.AppImage`-Zeile gemeldet.
</acceptance_criteria>
<verify>
<automated>cd /home/vicolab/projects/tessera-ctl && sh -n .gitea/scripts/desktop-collect.sh && sh -n .gitea/scripts/publish-release.sh && sh -n .gitea/scripts/desktop-version.sh && (cd apps/desktop/src-tauri && cargo check 2>&1 | tail -1 | grep -q Finished) && test "$(git rev-list --count --grep='ci(desktop): Runde' HEAD~6..HEAD)" -le 3 && echo ROUND-OK</automated>
<fails_when>Ein Skript hat einen Syntaxfehler, `cargo check` scheitert nach einer Cargo-Aenderung, oder es gibt mehr als drei Runden-Commits — `ROUND-OK` fehlt.</fails_when>
<human-check>Der Orchestrator bestaetigt nach der letzten Runde einen gruenen Lauf mit beiden Dateizeilen (Task 2).</human-check>
</verify>
<done>
Ein gruener Pipeline-Lauf auf `main` mit `Tessera-Setup-1.1.0-beta.{sha}.exe`
und `Tessera-1.1.0-beta.{sha}.AppImage` im Schritt "Pakete einsammeln" und
gruenem `publish`; hoechstens drei dokumentierte Runden.
</done>
</task>
</tasks>
<threat_model>
## Trust Boundaries
| Boundary | Description |
|----------|-------------|
| Runner -> Internet (rustup, crates.io, Microsoft-SDK ueber xwin, NSIS-Plugins von GitHub) | Der Cross-Bau laedt Werkzeuge und das Windows-SDK aus dem Netz. |
| Runner-Cache -> Bau | Aus dem Cache wiederhergestellte Artefakte (SDK, target/) fliessen in das Paket ein. |
| Gebautes `.exe` -> Anwender-PC | Unsigniert; SmartScreen warnt (D-09). |
## STRIDE Threat Register
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|-----------|----------|-----------|----------|-------------|-----------------|
| T-18-15 | Tampering | Werkzeugketten-Download (cargo-xwin, SDK, NSIS-Plugins) | medium | mitigate | `cargo install --locked cargo-xwin` (Lockfile des Werkzeugs), rustup von der offiziellen Adresse, SDK ueber cargo-xwin (prueft Microsoft-Manifest-Hashes), NSIS aus dem Ubuntu-Archiv; Tauri laedt seine NSIS-Plugins mit hinterlegten Pruefsummen. |
| T-18-16 | Tampering | Cache-Vergiftung (`target/`, xwin-Ablage) | low | accept | Der Cache-Server laeuft nur lokal fuer diesen Runner (`172.18.0.1`), keine fremden Schreiber; Schluessel haengt am `Cargo.lock`-Hash. |
| T-18-17 | Repudiation | Iterationsschleife | low | mitigate | Jede Runde ist ein eigener Commit mit Ursache im Titel und im SUMMARY dokumentiert. |
| T-18-18 | Information Disclosure | Protokollauszuege (Token) | low | mitigate | Gitea maskiert Secrets im Log; `publish-release.sh` gibt das Token nie aus (T-18-03). |
| T-18-SC | Tampering | `cargo install --locked cargo-xwin` (crates.io) | low | mitigate | Legitimitaetspruefung in RESEARCH: `OK` (rust-cross/cargo-xwin, seit 2022, 63k Downloads/Woche); kein `[ASSUMED]`/`[SUS]`, keine Sperr-Freigabe noetig. |
</threat_model>
<verification>
1. Statische Pruefung des Workflows (Kennzeichen, Reihenfolge AppImage vor
NSIS, Einsammeln mit beiden Plattformen).
2. Gruener Pipeline-Lauf auf `main` (Rueckmeldung des Orchestrators) mit
beiden Dateizeilen und gruenem `publish`.
3. Hoechstens drei dokumentierte Runden.
4. Nach dem Lauf traegt das Beta-Abbild die Pakete — sichtbar, sobald der
Nutzer den Testserver auf den neuen Stand zieht (18-06).
</verification>
<success_criteria>
- Der Job `desktop` erzeugt auf dem Linux-Runner `Tessera-Setup-X.Y.Z[…].exe`
und `Tessera-X.Y.Z[…].AppImage` in einem Lauf.
- Der Lauf ist gruen; `publish` hat Beta-Abbilder mit Paketen gepusht.
- Die Iterationsschleife ist dokumentiert und endete spaetestens nach drei
Runden.
</success_criteria>
<output>
Create `.planning/phases/18-desktop-client-fertigstellen/18-05-SUMMARY.md` when done.
Im SUMMARY festhalten: Dauer des ersten und (falls vorhanden) eines zweiten
Laufs mit warmem Cache, Groesse beider Dateien laut Sammel-Schritt, und die
Tabelle der Runden (Signatur, Ursache, Aenderung, Commit).
</output>