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

18 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, user_setup, estimate, must_haves
phase plan type wave depends_on files_modified autonomous requirements user_setup estimate must_haves
18-desktop-client-fertigstellen 02 execute 2
18-01
.gitea/workflows/ci.yml
.gitea/scripts/publish-images.sh
.gitea/scripts/publish-release.sh
true
DESK-01
DESK-04
tokens raw_tokens tasks confidence
45000 45000 2 low
truths artifacts key_links
Der CI-Job desktop laeuft nach test auf main und bei Tags v*, baut das Linux-AppImage mit der Tag-Version und uebergibt desktop-dist/ per actions/cache an publish (D-06, D-07).
publish bricht hart ab, wenn das Manifest aus dem Zwischenspeicher fehlt — nie ein Abbild ohne Pakete (D-08, Pitfall 1).
publish-release.sh haengt bei Tags jede Datei aus dem Manifest idempotent als Release-Datei an den Gitea-Release; das Token verlaesst nie die Header-Datei (D-01, D-08).
path provides contains
.gitea/workflows/ci.yml Job desktop (Linux-AppImage) und Uebergabe an publish per actions/cache desktop-dist-${{ gitea.sha }}
path provides contains
.gitea/scripts/publish-images.sh Harte Pruefung auf desktop-dist/manifest.json vor dem Docker-Bau manifest.json
path provides contains
.gitea/scripts/publish-release.sh Idempotenter Upload der Release-Dateien (GET assets, DELETE, POST multipart) upload_asset
from to via pattern
.gitea/workflows/ci.yml (desktop) .gitea/workflows/ci.yml (publish) actions/cache/save + actions/cache/restore mit Schluessel desktop-dist-${{ gitea.sha }}, fail-on-cache-miss: true fail-on-cache-miss
from to via pattern
.gitea/workflows/ci.yml (desktop) .gitea/scripts/desktop-version.sh + desktop-collect.sh Schritte 'Version setzen' und 'Pakete einsammeln' desktop-collect.sh --require linux
from to via pattern
.gitea/scripts/publish-release.sh desktop-dist/manifest.json jq -r '.files[].name' — nur Dateien aus dem Manifest werden hochgeladen files[]
Die in 18-01 lokal bewiesene Strecke wird in die Pipeline gehoben: ein neuer Job `desktop` baut auf `main` und bei Tags `v*` das Linux-AppImage mit der Tag-Version, sammelt es mit Manifest ein und uebergibt `desktop-dist/` per `actions/cache` an `publish`, das ohne Manifest hart abbricht und die Pakete ins API-Abbild kopiert. Bei Tags haengt `publish-release.sh` jede Datei aus dem Manifest an den Gitea-Release. Der Windows-Cross-Bau kommt in 18-05 in denselben Job; der echte Pipeline-Lauf wird dort mit beiden Dateien bewiesen.

Purpose: D-06, D-08 (Pipeline-Seite) und D-01 (Release-Dateien) aus 18-CONTEXT.md; Erfolgskriterium 1 (Linux-Haelfte und Release-Anhang). Output: Job desktop, angepasster Job publish, Manifest-Pruefung in publish-images.sh, Funktion upload_asset in publish-release.sh.

Externe Schnittstellen (Gitea REST): siehe 18-COVERAGE.md — neu sind GET …/releases/{id}/assets, DELETE …/releases/{id}/assets/{asset_id} und POST …/releases/{id}/assets?name= (multipart-Feld attachment); Gitea 1.26.2 laesst Release-Anhaenge standardmaessig ohne Typ-Beschraenkung bis 2048 MB zu.

Artifacts this phase produces

Dieser Plan: .gitea/workflows/ci.yml (Job desktop: Schritte "Systemabhaengigkeiten", "Rust-Toolchain", "Cargo-Zwischenspeicher", "Version setzen", "Rust pruefen", "Alte Bundles entfernen", "Linux-AppImage bauen", "Pakete einsammeln", "Uebergabe an publish"; Job publish: "Desktop-Pakete aus dem Zwischenspeicher holen", "Pakete pruefen"), .gitea/scripts/publish-images.sh (Manifest-Pruefung), .gitea/scripts/publish-release.sh (HDR_AUTH, upload_asset, DESKTOP_DIST). 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>

@.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-COVERAGE.md @.planning/phases/18-desktop-client-fertigstellen/18-01-SUMMARY.md

@.gitea/workflows/ci.yml @.gitea/scripts/publish-images.sh @.gitea/scripts/publish-release.sh @.gitea/scripts/desktop-collect.sh @.gitea/scripts/desktop-version.sh

Task 1: Job desktop (Linux-AppImage) und Uebergabe an publish per actions/cache Job-Aufbau und Cache-Schluessel lassen sich jederzeit aendern; kein Zustand ausserhalb des Runners. .gitea/workflows/ci.yml, .gitea/scripts/publish-images.sh .gitea/workflows/ci.yml, .gitea/scripts/publish-images.sh, .gitea/scripts/desktop-collect.sh (Optionen und Ausgabe, aus 18-01), .planning/phases/18-desktop-client-fertigstellen/18-RESEARCH.md (Abschnitte "Code Examples 2", "Common Pitfalls 1 und 5", "Standard Stack: Installation"), docs/ci-cd-setup.md (Abschnitt 4 "Pipeline-Ueberblick") **Job `desktop` in `.gitea/workflows/ci.yml`** zwischen `test` und `publish` einfuegen, Kopfkommentar der Datei um einen Satz zu Phase 18 ergaenzen. `name: Desktop-Pakete bauen`, `runs-on: ubuntu-latest`, `needs: test`, `if: gitea.ref == 'refs/heads/main' || startsWith(gitea.ref, 'refs/tags/v')` (D-06). Schritte in dieser Reihenfolge, deutsche Schrittnamen wie im Rest der Datei: `actions/checkout@v4` mit `fetch-depth: 0` (Tags fuer `git describe`); `actions/setup-node@v4` (Node 24); corepack/pnpm wie in `test`; `pnpm install --frozen-lockfile`; "Systemabhaengigkeiten": `sudo apt-get update` und `sudo apt-get install -y --no-install-recommends` mit **vollstaendiger** Liste `libwebkit2gtk-4.1-dev libjavascriptcoregtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev libgtk-3-dev libssl-dev patchelf file xdg-utils` (Pitfall 5 — das Runner-Abbild hat davon nur `librsvg2-dev` und `file`; alle Paketnamen wurden am 2026-09-16 per `apt-cache policy` im Abbild `gitea/runner-images:ubuntu-latest` bestaetigt, ebenso `sudo`, `jq`, `curl` und `git`); "Rust-Toolchain": `curl -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain stable` und danach `echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"` (kein Rust im Runner-Abbild; bewusst kein Fremd-Action, gleiche Zurueckhaltung wie beim Verzicht auf die Artefakt-Aktionen); "Cargo-Zwischenspeicher": `actions/cache@v4` mit `path` `~/.cargo/registry`, `~/.cargo/git`, `~/.cache/tauri`, `apps/desktop/src-tauri/target`, `key: desktop-cargo-${{ hashFiles('apps/desktop/src-tauri/Cargo.lock') }}`, `restore-keys: desktop-cargo-` (der Cache-Server des Runners ist laut RESEARCH aktiv: `172.18.0.1:42641`); "Version setzen": `sh .gitea/scripts/desktop-version.sh`; "Rust pruefen": `cargo check` und `cargo clippy` mit `working-directory: apps/desktop/src-tauri` (D-16; Clippy ohne `-D warnings`, Fehler brechen ab, Warnungen nicht); "Alte Bundles entfernen": `rm -rf apps/desktop/src-tauri/target/release/bundle` (ein aus dem Cache wiederhergestelltes altes AppImage wuerde sonst neben dem neuen liegen und das Sammel-Skript zu Recht abbrechen); "Linux-AppImage bauen": `pnpm --filter @tessera/desktop exec tauri build --bundles appimage`; "Pakete einsammeln": `sh .gitea/scripts/desktop-collect.sh --require linux` (18-05 erweitert auf `linux,windows`); "Uebergabe an publish": `actions/cache/save@v4` mit `path: desktop-dist` und `key: desktop-dist-${{ gitea.sha }}` (Pitfall 1: bewusst **nicht** die Artefakt-Aktionen von GitHub — auf dieser Gitea-Instanz dokumentiert unzuverlaessig; im Workflow-Kommentar ebenfalls nur so umschreiben, damit das Negativ-Tor in `` nicht am Kommentartext scheitert).

Job publish anpassen: needs: desktop statt needs: test. Nach dem Checkout und vor dem Registry-Login zwei Schritte: "Desktop-Pakete aus dem Zwischenspeicher holen" mit actions/cache/restore@v4, path: desktop-dist, key: desktop-dist-${{ gitea.sha }}, fail-on-cache-miss: true; "Pakete pruefen": test -f desktop-dist/manifest.json und jq . desktop-dist/manifest.json (harter Abbruch, nie stillschweigend ein Abbild ohne Pakete). Der Schritt mit publish-release.sh bleibt; die Pakete liegen fuer ihn unter desktop-dist/.

publish-images.sh: Im echten Bau-Pfad (nicht bei --print-plan) vor der Schleife pruefen, dass desktop-dist/manifest.json existiert, sonst Exit 1 mit Meldung — zweites Netz gegen Pitfall 1. Kopfkommentar um einen Absatz ergaenzen (Phase 18: die Pakete kommen aus dem Job desktop, das Dockerfile der API kopiert desktop-dist/). Weiterhin kein Secret. <acceptance_criteria> - grep -c '^ desktop:$' .gitea/workflows/ci.yml ergibt 1; grep -c 'needs: desktop' .gitea/workflows/ci.yml ergibt 1; grep -c 'fail-on-cache-miss: true' .gitea/workflows/ci.yml ergibt 1; grep -c 'desktop-dist-${{ gitea.sha }}' .gitea/workflows/ci.yml ergibt 2 (save und restore). - grep -c 'upload-artifact' .gitea/workflows/ci.yml ergibt 0. - grep -c 'libwebkit2gtk-4.1-dev' .gitea/workflows/ci.yml ergibt mindestens 1; grep -c 'desktop-collect.sh --require linux' .gitea/workflows/ci.yml ergibt 1; grep -c 'desktop-version.sh' .gitea/workflows/ci.yml ergibt 1. - sh -n .gitea/scripts/publish-images.sh endet mit 0; GITHUB_REF=refs/tags/v1.1.0 sh .gitea/scripts/publish-images.sh --print-plan gibt weiterhin die vier push-Zeilen aus (Probelauf braucht kein Manifest). - grep -c 'manifest.json' .gitea/scripts/publish-images.sh ergibt mindestens 1. </acceptance_criteria> cd /home/vicolab/projects/tessera-ctl && test "$(grep -c 'desktop-dist-${{ gitea.sha }}' .gitea/workflows/ci.yml)" = "2" && grep -q 'fail-on-cache-miss: true' .gitea/workflows/ci.yml && grep -q 'needs: desktop' .gitea/workflows/ci.yml && test "$(grep -c 'upload-artifact' .gitea/workflows/ci.yml)" = "0" && grep -q 'desktop-collect.sh --require linux' .gitea/workflows/ci.yml && grep -q 'desktop-version.sh' .gitea/workflows/ci.yml && node -e "const y=require('fs').readFileSync('.gitea/workflows/ci.yml','utf8');if(!/^ desktop:\n/m.test(y)||!/^ publish:\n/m.test(y))process.exit(1)" && echo CI-OK <fails_when>Cache-Schluessel nicht genau zweimal, Restore ohne harten Abbruch, publish haengt nicht an desktop, ein upload-artifact-Schritt ist vorhanden, Skript-Schritte fehlen, oder die Job-Schluessel fehlen — CI-OK fehlt.</fails_when> cd /home/vicolab/projects/tessera-ctl && sh -n .gitea/scripts/publish-images.sh && GITHUB_REF=refs/tags/v1.1.0 sh .gitea/scripts/publish-images.sh --print-plan | grep -c '^push ' | grep -qx 4 && grep -q 'manifest.json' .gitea/scripts/publish-images.sh && echo IMAGES-OK <fails_when>Syntaxfehler, weniger als vier push-Zeilen im Probelauf, oder die Manifest-Pruefung fehlt im Skript — IMAGES-OK fehlt.</fails_when> Der Workflow enthaelt den Job desktop (Linux-AppImage mit Tag-Version, Cache, Uebergabe per actions/cache), publish haengt daran und bricht ohne Manifest ab; publish-images.sh prueft das Manifest ebenfalls.

Task 2: Release-Dateien idempotent an den Gitea-Release haengen Das Gitea-Secret `REGISTRY_TOKEN` traegt `repository: write` (damit wurde am 2026-09-16 der Release v1.1.0 aus der Pipeline angelegt); es wird unveraendert weiterverwendet. Lokal liegt `desktop-dist/manifest.json` aus 18-01 vor (fuer den Probelauf). .gitea/scripts/publish-release.sh .gitea/scripts/publish-release.sh (gesamt — Idempotenz-Muster GET -> case -> PATCH/POST, Header-Datei-Mechanik ab Zeile 117), .planning/phases/18-desktop-client-fertigstellen/18-RESEARCH.md (Abschnitte "Code Examples 6", "Don't Hand-Roll", "Security Domain"), .planning/phases/18-desktop-client-fertigstellen/18-COVERAGE.md, desktop-dist/manifest.json (Form der `files`-Eintraege) Kopfkommentar um Umgebung `DESKTOP_DIST` (Vorgabe `desktop-dist`) und die drei neuen Endpunkte ergaenzen. Neben `$HDR` (mit JSON-Content-Type) eine zweite Header-Datei `$HDR_AUTH` anlegen, die **nur** die `Authorization`-Zeile traegt — beim multipart-Upload darf kein `Content-Type: application/json` mitgehen; gleiche `umask 077`/`mktemp`/ `trap`-Mechanik, Token nie als Argument (T-18-03). Funktion `upload_asset FILE NAME RELEASE_ID` nach dem Muster GET -> Entscheidung per HTTP-Code -> Aktion: `GET $RELEASES_URL/$ID/assets` (200 erwartet), per `jq -r --arg n "$NAME" '.[] | select(.name == $n) | .id'` vorhandene Datei gleichen Namens ermitteln und mit `DELETE $RELEASES_URL/$ID/assets/$ASSET_ID` entfernen (204 erwartet), dann `curl -sS --header @"$HDR_AUTH" -X POST -F "attachment=@$FILE;filename=$NAME" -o "$RESP" -w '%{http_code}' "$RELEASES_URL/$ID/assets?name=$NAME"` (201 erwartet; jeder andere Code: Meldung mit Code und Antwort nach stderr, Exit 1). Aufruf nach dem bestehenden `case`-Block (Release angelegt oder aktualisiert; `ID` aus beiden Zweigen verfuegbar machen): Manifest `$DESKTOP_DIST/manifest.json` muss existieren, sonst Exit 1 (Release-Text ist dann schon da, der Job wird sichtbar rot); fuer jeden Namen aus `jq -r '.files[].name'` `upload_asset "$DESKTOP_DIST/$NAME" "$NAME" "$ID"`, danach je Datei `Release-Datei $NAME hochgeladen`. `--dry-run` listet zusaetzlich die geplanten Uploads (`POST $RELEASES_URL/{id}/assets?name=…`) aus dem Manifest, falls es vorhanden ist.

Bekannter Fallstrick fuer 18-05: Der Job-Container erreicht Gitea ueber https://git.vicolab.de hinter dem Nginx Proxy Manager; das AppImage ist rund 106 MB — falls der Proxy den Upload abweist (413), kann GITEA_API im Workflow-Schritt auf die Host-Adresse http://172.18.0.1:3002/api/v1 gesetzt werden (gleiche Route, ueber die der Runner seinen Cache-Server erreicht). Das wird erst im CI-Lauf entschieden, nicht hier. Der Upload-Pfad selbst laeuft erst beim naechsten Freigabe-Tag (ein Test-Tag wuerde den Live-Kanal ausloesen) — deshalb ist der Probelauf mit --dry-run hier das Tor. <acceptance_criteria> - sh -n .gitea/scripts/publish-release.sh endet mit 0. - sh .gitea/scripts/publish-release.sh --dry-run --tag v1.1.0 gibt eine Zeile mit assets?name=Tessera-1.1.0.AppImage aus (Manifest aus 18-01 vorhanden) und endet mit 0; ohne Token, ohne Netzaufruf. - grep -c 'HDR_AUTH' .gitea/scripts/publish-release.sh ergibt mindestens 3 (Anlegen, Schreiben, Verwendung); grep -c '^upload_asset()' .gitea/scripts/publish-release.sh ergibt 1. - grep -c "files\[\].name" .gitea/scripts/publish-release.sh ergibt mindestens 1. - Das Token wird nirgends als Argument uebergeben: grep -c 'token %s' .gitea/scripts/publish-release.sh ergibt genau 1 (die bestehende printf-Zeile in die Header-Datei) oder 2 (zweite Header-Datei), nie in einer curl-Zeile. </acceptance_criteria> cd /home/vicolab/projects/tessera-ctl && sh -n .gitea/scripts/publish-release.sh && sh .gitea/scripts/publish-release.sh --dry-run --tag v1.1.0 | grep -q 'assets?name=Tessera-1.1.0.AppImage' && test "$(grep -c 'HDR_AUTH' .gitea/scripts/publish-release.sh)" -ge 3 && grep -q '^upload_asset()' .gitea/scripts/publish-release.sh && grep -q 'files[].name' .gitea/scripts/publish-release.sh && test "$(grep -c 'curl.*GITEA_TOKEN' .gitea/scripts/publish-release.sh)" = "0" && echo RELEASE-OK <fails_when>Syntaxfehler, der Probelauf nennt den AppImage-Upload nicht, die zweite Header-Datei oder die Funktion fehlt, die Dateinamen kommen nicht aus dem Manifest, oder das Token steht in einer curl-Zeile — RELEASE-OK fehlt.</fails_when> Das Release-Skript laedt alle Manifest-Dateien idempotent hoch (vorhandene Datei gleichen Namens wird ersetzt), das Token bleibt in Header-Dateien, der Probelauf nennt die geplanten Uploads. Der echte Pipeline-Beweis folgt in 18-05 (gemeinsam mit Windows), der Release-Anhang beim naechsten Freigabe-Tag.

<threat_model>

Trust Boundaries

Boundary Description
CI-Runner -> Gitea-API (Release-Dateien) Ausgehender Aufruf mit dem Zugriffstoken REGISTRY_TOKEN.
Runner -> Cache-Server (actions/cache) Uebergabe der Pakete zwischen zwei Jobs desselben Laufs.
Runner -> Internet (rustup, crates.io, Tauri-Werkzeuge) Der Job laedt Werkzeuge aus dem Netz.

STRIDE Threat Register

Threat ID Category Component Severity Disposition Mitigation Plan
T-18-03 Information Disclosure publish-release.sh (Token) high mitigate Token nur aus der Umgebung, nie als Argument, nur ueber Header-Dateien mit umask 077; keine Ausgabe des Tokens; zweite Header-Datei ohne JSON-Content-Type fuer multipart. Gate: keine curl-Zeile enthaelt GITEA_TOKEN.
T-18-06 Tampering publish ohne Pakete (Cache-Fehlschlag) medium mitigate fail-on-cache-miss: true plus expliziter test -f desktop-dist/manifest.json im Workflow und in publish-images.sh.
T-18-21 Tampering Cache-Uebergabe zwischen Jobs (desktop-dist-{sha}) low accept Cache-Server nur lokal fuer diesen Runner (172.18.0.1), Schluessel exakt am Commit-SHA, keine restore-keys-Fallbacks fuer die Uebergabe.
T-18-SC Tampering Paketinstallationen (actions/cache@v4, actions/checkout@v4, actions/setup-node@v4; Rust-Toolchain per rustup) low mitigate Nur GitHub-eigene Actions in der bereits genutzten Major-Version; rustup-Installer von der offiziellen Adresse; keine neuen npm/pip/cargo-Pakete in diesem Plan.
</threat_model>
1. `ci.yml` enthaelt Job `desktop`, `publish` mit `needs: desktop`, Cache-Restore mit hartem Abbruch, kein upload-artifact. 2. `publish-images.sh` und `publish-release.sh` bestehen `sh -n`; Probelaeufe zeigen die erwarteten Zeilen (`push` x4, `assets?name=Tessera-1.1.0.AppImage`). 3. Kein `curl`-Aufruf traegt das Token als Argument. 4. Der echte Lauf wird in 18-05 bewiesen; der Release-Anhang beim naechsten Tag (18-06, human-check Punkt b).

<success_criteria>

  • Job desktop baut das AppImage mit Tag-Version und uebergibt es per Cache.
  • publish kann kein Abbild ohne Pakete mehr bauen.
  • Release-Dateien werden bei Tags idempotent aus dem Manifest hochgeladen. </success_criteria>
Create `.planning/phases/18-desktop-client-fertigstellen/18-02-SUMMARY.md` when done.