Files
tessera-ctl/docs/ci-cd-setup.md
T

27 KiB

Tessera CI/CD Setup

Runbook fuer die Einrichtung der Gitea-basierten CI/CD-Pipeline.

Voraussetzungen

  • Docker und Docker Compose installiert
  • Gitea-Instanz mit aktivierten Actions
  • Zugriff auf die Gitea-Administrationsoberflaeche

1. Gitea-Repository einrichten

  1. In Gitea ein neues Repository erstellen (z.B. tessera-ctl)
  2. Das Repository als Git-Remote hinzufuegen:
git remote add origin https://<gitea-url>/<user>/tessera-ctl.git
  1. In den Repository-Einstellungen unter Settings > Actions sicherstellen, dass Actions aktiviert ist.

2. act_runner registrieren

Der act_runner fuehrt Gitea Actions Workflow-Jobs in Docker-Containern aus.

Runner-Token generieren

  1. In Gitea navigieren zu: Repository > Settings > Actions > Runners (oder Admin-Panel > Actions > Runners fuer globale Runner)
  2. Create registration token klicken
  3. Token kopieren

Runner starten

Die Umgebungsvariablen setzen (z.B. in .env im Projektverzeichnis oder direkt in der Shell):

export GITEA_INSTANCE_URL=https://git.vicolab.de
export GITEA_RUNNER_REGISTRATION_TOKEN=<token-aus-schritt-oben>

Runner starten:

docker compose -f docker-compose.ci.yml up -d

Runner-Status pruefen:

docker compose -f docker-compose.ci.yml logs act_runner

Der Runner registriert sich automatisch bei Gitea und ist bereit, Jobs auszufuehren.

Bestehender Runner

Wenn bereits ein act_runner als separater Docker-Container laeuft (z.B. gitea-runner), kann docker-compose.ci.yml als Dokumentation und Vorlage fuer eine Neueinrichtung verwendet werden. Der bestehende Runner muss nicht ersetzt werden.

Bestehenden Runner pruefen:

docker ps --filter name=gitea-runner

3. Erforderliche Konfiguration

Umgebungsvariablen

Variable Beschreibung Wo setzen
GITEA_INSTANCE_URL URL der Gitea-Instanz .env oder Systemumgebung
GITEA_RUNNER_REGISTRATION_TOKEN Runner-Registrierungstoken .env oder Systemumgebung

Gitea Secrets (fuer die CI-Pipeline)

In Gitea unter Repository > Settings > Actions > Secrets werden die Secrets fuer die Pipeline konfiguriert. Benoetigt werden drei:

Secret Beschreibung
REGISTRY_TOKEN Gitea-Zugangstoken (Access Token) mit Schreibrecht auf Pakete (package: write) und zusaetzlich auf das Repository (repository: write, fuer Releases). Wird im Job publish fuer docker login localhost:3002 --password-stdin verwendet und im Release-Schritt ueber env als GITEA_TOKEN an .gitea/scripts/publish-release.sh gereicht -- nie als Argument.
TAURI_SIGNING_PRIVATE_KEY Inhalt der privaten Schluesseldatei des Tauri-Updaters (eine Base64-Zeile, erzeugt mit tauri signer generate). Wird ausschliesslich an den zwei tauri build-Schritten des Jobs desktop als env gesetzt; die Tauri-CLI signiert damit das AppImage und den Windows-Installer (.sig neben dem Bundle). Der oeffentliche Gegenpart steht in apps/desktop/src-tauri/tauri.conf.json (plugins.updater.pubkey).
TAURI_SIGNING_PRIVATE_KEY_PASSWORD Passwort zu diesem Schluessel; gleiche Stelle, gleicher Umfang.

Das Token erscheint nie im Log: es wird per --password-stdin uebergeben und Gitea maskiert Secret-Werte in der Job-Ausgabe. Das Veroeffentlichungs-Skript .gitea/scripts/publish-images.sh kennt das Token nicht; der Login bleibt im Workflow.

Der Signierschluessel wird ebenfalls nie ausgegeben: die Skripte kennen ihn nicht (desktop-collect.sh prueft nur, OB die Variable gesetzt ist, um die Signatur zur Pflicht zu machen), nur die beiden Bau-Schritte sehen ihn -- weder pnpm install, apt-get, cargo install cargo-xwin noch die Cache-Schritte. Die Sicherung des Schluessels ausserhalb der Pipeline (~/.tessera/desktop-updater/ auf dem Entwicklungsrechner) beschreibt das Betriebshandbuch, Kapitel 10.

Der Push geht ueber localhost:3002 (Gitea laeuft auf demselben Rechner wie der Runner), weil der Nginx Proxy Manager vor git.vicolab.de grosse Image-Blobs blockt. Das Pullen auf den Servern laeuft ueber git.vicolab.de (docker-compose.prod.yml).

Weitere Secrets bei Bedarf:

  1. In Gitea Settings > Actions > Secrets den Secret anlegen
  2. In .gitea/workflows/ci.yml ueber ${{ secrets.SECRET_NAME }} referenzieren

Wichtig: Secrets niemals in Logs ausgeben oder in Workflow-Dateien hardcoden.

4. Pipeline-Ueberblick

Die CI/CD-Pipeline (.gitea/workflows/ci.yml) laeuft bei jedem Push auf die Zweige main und live sowie bei jedem Tag v* (z. B. v1.0.0) und besteht aus fuenf Jobs (vier bauen aufeinander auf, der fuenfte ist ein reiner Bericht):

  1. quality -- Lint und TypeScript Type-Check (Lint ist derzeit ein Leerlauf, siehe WINDOWS #35; der Type-Check ist echt)
  2. test -- Vitest Unit- und Integrationstests
  3. desktop -- Desktop-Pakete fuer Windows und Linux bauen (nur auf main und bei Tags v*, siehe unten)
  4. publish -- Docker Images mit Versionsstempel bauen und in die Gitea-Registry veroeffentlichen
  5. security -- Sicherheitspruefung (nur Bericht): nach publish, nur auf main und bei Tags v*; bricht nie ab, blockiert nie etwas, bekommt kein Secret

Ablauf: quality -> test -> desktop -> publish -> security (jeder Job nur bei Erfolg des vorherigen; security ist nur ein Bericht und steht in keinem needs eines anderen Jobs; desktop selbst laeuft nur, wenn die if-Bedingung zutrifft -- auf einem Push nach live ohne Tag entfaellt der Job, publish startet in diesem Fall trotzdem, weil needs: desktop bei einem uebersprungenen Job nicht blockiert). Der Job publish besteht aus sechs Schritten: actions/checkout@v4 mit fetch-depth: 0 (volle Historie samt Tags, sonst liefert git describe nichts), der Wiederherstellung der Desktop-Pakete aus dem Zwischenspeicher, einer harten Pruefung des Manifests, Login in die Registry (siehe Abschnitt 3), der Aufruf von .gitea/scripts/publish-images.sh und der Aufruf von .gitea/scripts/publish-release.sh (legt bei Tags v* den Gitea-Release aus dem CHANGELOG-Abschnitt an und haengt die Desktop-Pakete als Dateien an; auf main endet er mit "nichts zu tun").

Job desktop: Windows- und Linux-Pakete auf dem Linux-Runner

Der Job desktop baut auf demselben ubuntu-latest-Runner nacheinander (ein Cache, ein Runner, siehe 18-CONTEXT.md Specific Ideas) sowohl das Linux-AppImage als auch -- per Cross-Bau -- den Windows-Installer:

Ueberspringen bei unveraendertem Desktop (quick-260917-jdh): Direkt nach dem Checkout berechnet desktop-stamp.sh stamp den Stempel <Version>-<SHA> (Version aus desktop-version.sh --print; SHA = letzter Commit an apps/desktop, desktop-version.sh, desktop-collect.sh, desktop-stamp.sh, ci.yml -- Konstante DESKTOP_PATHS; pnpm-lock.yaml bewusst nicht, weil die Tauri-CLI-Version an apps/desktop/package.json haengt und der Bau keine Datei ausserhalb von apps/desktop liest) und gibt skip_allowed=true nur fuer refs/heads/main aus. Danach actions/cache/restore@v4 mit dem Schluessel desktop-dist-stamp-<Stempel> -- ohne restore-keys, weil act_runner auch den Hauptschluessel per Praefix sucht und ein aelterer Stand nie als Treffer gelten darf. Anschliessend desktop-stamp.sh check: cache-hit, Manifest, Kanal beta, Version, beide Dateien mit Groesse und sha256 laut Manifest -> reuse=true; sonst raeumt es desktop-dist/ auf und gibt reuse=false. Alle Bau-Schritte (setup-node bis Pakete einsammeln) tragen if: steps.reuse.outputs.reuse != 'true'. Nach einem echten Bau legt actions/cache/save@v4 die Pakete zusaetzlich unter dem Stempel-Schluessel ab (nur auf main). Die Uebergabe an publish (desktop-dist-<sha>) laeuft in beiden Faellen; publish ist unveraendert. Bei Tags v* wird weder gesucht noch abgelegt. Lokale Probe: DESKTOP_TAG=v1.2.0 GITHUB_REF=refs/heads/main sh .gitea/scripts/desktop-stamp.sh stamp.

  1. Systemabhaengigkeiten (apt-get install): WebKit/Tauri-Pakete (libwebkit2gtk-4.1-dev usw.) fuer den Linux-Bau, dazu lld llvm clang nsis fuer den Windows-Cross-Bau (der NSIS-Bundler ruft makensis aus genau diesem Paket auf).
  2. Rust-Toolchain per rustup (kein Rust im Runner-Abbild), inklusive rustup component add clippy -- --profile minimal installiert clippy sonst nicht mit (siehe Fehlerbehebung unten).
  3. Cargo-Zwischenspeicher (actions/cache@v4, Schluessel ueber den Hash von Cargo.lock): ~/.cargo/registry, ~/.cargo/git, ~/.cargo/bin/cargo-xwin, ~/.cache/cargo-xwin (die von cargo-xwin heruntergeladene Windows-SDK-Ablage -- soll nur einmal geladen werden), ~/.local/share/tauri (NSIS-Plugins) und apps/desktop/src-tauri/target.
  4. Windows-Werkzeuge -- bewusst NACH dem Cache-Wiederherstellungsschritt: rustup target add x86_64-pc-windows-msvc und command -v cargo-xwin || cargo install --locked cargo-xwin. Stuende dieser Schritt vor der Cache-Wiederherstellung, wuerde cargo-xwin bei jedem Lauf neu gebaut, selbst wenn der Cache es bereits enthaelt.
  5. Version setzen (desktop-version.sh), Rust pruefen (cargo check/cargo clippy, D-16), Bau des Linux-AppImage (tauri build --bundles appimage), dann des Windows-Installers per Cross-Bau (tauri build --runner cargo-xwin --target x86_64-pc-windows-msvc --bundles nsis). Beide Bau-Schritte tragen die Secrets TAURI_SIGNING_PRIVATE_KEY/_PASSWORD als env und signieren die Bundles (quick-260917-kgc): die Tauri-CLI legt .sig-Dateien neben -setup.exe und .AppImage ab -- host-unabhaengig, also auch im Cross-Bau. Kein --no-sign im CI.
  6. Pakete einsammeln (desktop-collect.sh --require linux,windows) -- schreibt manifest.json (mit updateVersion und je Plattform der signature aus der .sig-Datei) und schlaegt fehl, wenn eine der beiden Dateien fehlt oder auf main/Tags eine .sig fehlt.
  7. Uebergabe an publish per actions/cache/save@v4 mit dem Schluessel desktop-dist-${{ gitea.sha }} (ein neuer Schluessel je Commit, damit publish garantiert die Pakete des gerade gebauten Standes bekommt, nicht einen aelteren Cache-Treffer). Dieser Schritt laeuft auch dann, wenn der Bau uebersprungen wurde -- er sichert in diesem Fall den aus dem Stempel-Cache restaurierten Stand unter dem neuen SHA.

Warum actions/cache und nicht upload-artifact: Auf dieser Gitea-Instanz ist actions/upload-artifact/download-artifact unzuverlaessig (Erfahrungswert aus 18-02) -- die Uebergabe zwischen desktop und publish laeuft deshalb bewusst ueber actions/cache/save und actions/cache/restore mit fail-on-cache-miss: true, nicht ueber Artefakt-Uploads.

Das Release-Skript spricht die Gitea-API NIE ueber die oeffentliche Adresse (GITHUB_API_URL/GITHUB_SERVER_URL = https://git.vicolab.de hinter dem Proxy, der grosse Uploads abbricht -- so blieb Release 1.2.0 am 2026-09-17 zunaechst ohne Anhaenge). Im Job-Container ist localhost:3002 nicht der Host; das Skript ermittelt deshalb das Host-Gateway aus /proc/net/route (z. B. 172.17.0.1) und ruft http://<gateway>:3002/api/v1 auf -- derselbe Weg wie der Registry-Push. Lokal nimmt es http://localhost:3002/api/v1; GITEA_API bleibt als expliziter Override. Mit --dry-run --tag vX.Y.Z laesst sich die gewaehlte Adresse ohne Netzaufruf und ohne Token pruefen.

Zwei Kanaele: Etiketten je Anlass

Das Skript .gitea/scripts/publish-images.sh entscheidet allein anhand GITHUB_REF, ob und unter welchen Etiketten veroeffentlicht wird:

Anlass Kanal (APP_CHANNEL) Etiketten in der Registry
Push auf main beta beta und latest (latest ist nur ein Alias fuer beta und entfaellt spaeter)
Tag vX.Y.Z live live und vX.Y.Z + Gitea-Release Tessera X.Y.Z mit dem CHANGELOG-Abschnitt
Push auf live ohne Tag -- keine; der Lauf prueft nur (quality, test), das Skript endet mit "nichts zu tun"

Das Kanalmodell fuer den Betrieb (welcher Server welches Etikett zieht, Freigabe, Hotfix) steht in docs/anleitung-betrieb.md, Kapitel 9.

Versionsstempel

Das Skript berechnet vier Werte und gibt sie als --build-arg an beide Dockerfiles (apps/web/Dockerfile, apps/api/Dockerfile):

Build-Arg Quelle
APP_VERSION git describe --tags --always (ohne Tag: kurzer Commit-SHA)
APP_CHANNEL beta oder live, siehe Tabelle oben
APP_COMMIT git rev-parse --short HEAD
APP_BUILD_TIME date -u, ISO-Format

Die API liest die Werte zur Laufzeit (GET /health/version, Startzeile im Log). Das Web-Image bettet NEXT_PUBLIC_APP_* beim Build in das Browser-Bundle ein -- deshalb laeuft die builder-Stufe des Web-Images jetzt bei jedem Pipeline-Lauf neu, die Laufzeit liegt eher bei 4-6 statt 2 Minuten.

Lokale Probe ohne Docker-Aufruf:

GITHUB_REF=refs/tags/v1.2.3 sh .gitea/scripts/publish-images.sh --print-plan

Hinweis: Die fruehere Entscheidung D-13 (Images lokal bauen, keine Registry) ist ueberholt -- seit der Einfuehrung der Gitea-Registry werden Images gepusht und von den Servern per docker compose pull geholt.

Job security: Sicherheitspruefung (nur Bericht)

Der Job security (quick-261009-p0m) laeuft nach publish, damit er die Veroeffentlichung nie verzoegert oder verhindert. Er laeuft nur auf main und bei Tags v*; die Bedingung steht ausdruecklich am Job, weil ein uebersprungenes needs in Gitea nicht blockiert (sonst liefe er auch nach einem Push auf live). Er hat continue-on-error, ein Zeitlimit von 30 Minuten (ein haengender Scan haelt den einzigen Runner nicht fest) und kein Secret. Das Skript haelt sich selbst an ein Zeitbudget von 25 Minuten (siehe "Zeitbudget" unten). Alles steckt in einem Skript, das auch lokal laeuft:

GITHUB_REF=refs/heads/main sh .gitea/scripts/security-scan.sh --print-plan   # ohne Netz
GITHUB_REF=refs/heads/main sh .gitea/scripts/security-scan.sh all

Was geprueft wird: gitleaks ueber die gesamte Git-Historie (Zugangsdaten), pnpm audit --prod und osv-scanner ueber pnpm-lock.yaml und die Desktop-Cargo.lock (bekannte Schwachstellen), Semgrep (statische Code-Analyse) und Trivy ueber den Quellstand sowie ueber die frisch gebauten Abbilder api und web (main -> :beta, Tag v* -> :live). Die Abbilder liegen nach publish im Docker-Daemon des Hosts (Socket-Mount, Abschnitt 5) und werden direkt von dort gelesen.

Quellstand: Die Quellpruefungen lesen einen git archive HEAD-Export in einem temporaeren Ordner, gitleaks nur die Git-Historie. Es wird also genau der eingecheckte Stand geprueft; nicht eingecheckte Dateien eines Arbeitsordners erreichen nie ein Werkzeug, einen Bericht oder ein Artefakt.

Angepinnte Werkzeuge: gitleaks 8.30.1, Trivy 0.75.0, osv-scanner 2.6.0, Semgrep 1.180.0, pnpm 9.15.0. Die drei Programme werden von der offiziellen GitHub-Veroeffentlichung geladen und vor jeder Benutzung gegen eine SHA256-Summe im Skript geprueft (auch das zwischengespeicherte Archiv); stimmt die Summe nicht oder fehlt das Netz, wird nur dieses Werkzeug uebersprungen. Semgrep laeuft als Container aus dem offiziellen Abbild semgrep/semgrep, angepinnt per Digest (SEMGREP_IMAGE im Skript): Docker prueft beim Laden, dass der Inhalt zum Digest passt. Es gibt bewusst keine Installation ueber pipx/PyPI mehr -- deren frei aufgeloeste Abhaengigkeiten liefen sonst in einem Job, der den Docker-Socket des Hosts sieht. Der Quellstand geht per docker cp in den Container und der Bericht per docker cp zurueck (kein Ordner-Mount, denn der Arbeitsordner liegt im Job-Container und nicht auf dem Docker-Rechner). Das Abbild belegt rund 1,5 GB im Docker-Daemon des Runners und bleibt dort zwischengespeichert; ohne Docker wird Semgrep mit kein-docker uebersprungen. Marktplatz-Aktionen fuer Scanner werden bewusst nicht benutzt (veraenderliche Etiketten). Version anheben: im Kopf von security-scan.sh die Version, die URL und die SHA256-Summe aus der offiziellen Pruefsummendatei der Veroeffentlichung gemeinsam austauschen und einmal lokal sh .gitea/scripts/security-scan.sh all laufen lassen. Bei Semgrep stattdessen SEMGREP_VERSION und den Digest in SEMGREP_IMAGE tauschen (den Digest zeigt docker pull semgrep/semgrep:<Version> in der Zeile Digest:). Werkzeuge und die Trivy-Datenbank liegen im persistenten Zwischenspeicher des Runners (/opt/hostedtoolcache/tessera-security); der Layer-Zwischenspeicher von Trivy wird nach jedem Lauf geloescht, damit die Platte nicht vollaeuft.

Ausnahmelisten: .gitleaks.toml (geprueft harmlose Treffer: Testschluessel des Zertifikatsmanagers, Schluessel-Ausschnitte in Tests und Notizen) und .semgrepignore (Tests, Testdaten, Planungsnotizen). Ein neuer Treffer wird nie vorsorglich freigegeben, sondern erst geprueft.

Zeitbudget: Der Job hat 30 Minuten (timeout-minutes). Das Skript rechnet mit einem Gesamtbudget von 1500 Sekunden (25 Minuten) und jedes Werkzeug hat ein eigenes Limit (Variablen T_* im Kopf des Skripts): Laden der drei Programme je 60 s, Semgrep-Abbild laden 150 s, pnpm 45 s, gitleaks 180 s, pnpm audit 90 s, osv-scanner 120 s, Semgrep 300 s, Trivy ueber den Quellstand 180 s, je Abbild 120 s -- zusammen hoechstens 1485 Sekunden. Zusaetzlich kuerzt das Skript jedes Limit auf die noch verbleibende Gesamtzeit; ist nichts mehr uebrig, werden die restlichen Werkzeuge mit skipped reason=gesamtbudget gemeldet. Ein Werkzeug, das sein Limit reisst, erscheint als skipped reason=zeitgrenze. Die fuenf Minuten Reserve bis zu den 30 Minuten decken Checkout und Artefakt-Schritt ab. Jedes Werkzeug gibt seine Zeile aus, sobald es fertig ist -- bricht der Runner den Job trotzdem einmal ab, stehen die bis dahin fertigen Ergebnisse im Protokoll.

Wo man das Ergebnis sieht: Im Protokoll des Jobs stehen je Werkzeug eine Zeile SECURITY-SUMMARY ... (nur Zahlen), die Zeile SECURITY-SUMMARY dauer=... und am Ende SECURITY-SUMMARY fertig (nur Bericht, Exit 0); dieselben Zeilen stehen in summary.txt. Prueft das Skript in einem flachen Klon (ohne volle Historie), steht bei gitleaks unvollstaendig (flacher Klon ...) -- dann ist das Ergebnis nicht "sauber", sondern nur fuer die sichtbaren Commits gueltig (die Pipeline holt mit fetch-depth: 0 immer die volle Historie). Die Rohberichte (JSON) haengen, soweit der Runner es zulaesst, als Artefakt sicherheitsberichte (30 Tage) am Lauf. Das Artefakt ist nur "best effort" (Gitea lehnt upload-artifact@v4 ab, es laeuft @v3); verlassen Sie sich auf die Zeilen im Protokoll. Der Job braucht auf dem Entwicklungsrechner mit warmem Zwischenspeicher etwa eine Minute; beim allerersten Lauf auf einem frischen Runner kommen das Laden der Programme und des Semgrep-Abbilds (rund 1,5 GB) und die Trivy-Datenbank dazu. Mehr als 25 Minuten laufen nie (siehe "Zeitbudget").

5. Sicherheitshinweise

Docker Socket

Der act_runner mountet den Host-Docker-Socket (/var/run/docker.sock), um Jobs in Docker-Containern ausfuehren zu koennen. Das bedeutet:

  • Der Runner hat Zugriff auf den Docker-Daemon des Hosts
  • Er kann Container starten, stoppen und inspizieren

Risikobewertung: Akzeptabel fuer interne CI, da:

  • Nur Claude pusht manuell bei Meilensteinen (D-11)
  • Kein externer Code wird ausgefuehrt
  • Ephemeral Mode ist aktiviert (siehe unten)

Ephemeral Runner

GITEA_RUNNER_EPHEMERAL=1 sorgt dafuer, dass der Runner seine Credentials nach jedem Job widerruft und sich neu registriert. Das verhindert:

  • Persistente Zugriffstokens im Runner-Container
  • Kompromittierung ueber alte Job-Artefakte
  • Seitliche Bewegung zwischen Jobs

Workflow-Dateien

Workflow-Dateien in .gitea/workflows/ werden direkt aus dem Repository geladen. Da nur vertrauenswuerdiger Code gepusht wird (D-11, Claude als einziger Committer), ist das Risiko einer manipulierten Pipeline minimal.

Ein Tag-Push v* ist der Freigabe-Hebel fuer Live: wer ihn setzen darf, kann das live-Etikett neu belegen. Heute hat nur das Konto schalli Schreibrecht (0 Kollaborateure, keine Branch-Regeln). Kommen weitere Konten dazu, in Gitea unter Repository > Settings > Branches / Tags eine Tag-Schutzregel fuer v* und einen Branch-Schutz fuer live anlegen (T-KU1-04).

6. Fehlerbehebung

Runner registriert sich nicht

  1. Pruefen ob GITEA_INSTANCE_URL korrekt und erreichbar ist
  2. Token ggf. neu generieren (Token sind einmalig verwendbar bei Ephemeral Mode)
  3. Runner-Logs pruefen: docker compose -f docker-compose.ci.yml logs act_runner

Pipeline startet nicht

  1. In Gitea pruefen ob Actions fuer das Repository aktiviert ist
  2. Pruefen ob der Runner als "online" angezeigt wird (Gitea > Actions > Runners)
  3. Sicherstellen dass die Workflow-Datei unter .gitea/workflows/ liegt
  4. YAML-Syntax validieren

Docker-Build schlaegt fehl

  1. Sicherstellen dass docker-compose.yml im Arbeitsverzeichnis des Runners erreichbar ist
  2. Docker Daemon Status pruefen: docker info
  3. Disk Space pruefen: df -h

Stempel zeigt dev oder nur eine Kurzkennung statt des Tags

  1. Im Job publish pruefen, dass actions/checkout@v4 mit fetch-depth: 0 auscheckt -- ohne Tags liefert git describe --tags --always nur den SHA
  2. Pruefen, ob der Tag wirklich gepusht wurde: git ls-remote --tags origin
  3. dev bedeutet: das Image wurde ohne Build-Args gebaut (lokal statt ueber das Skript) -- das ist fuer lokale Builds normal

Job desktop schlaegt fehl

  1. cargo clippy meldet 'cargo-clippy' is not installed for the toolchain -- der Schritt "Rust-Toolchain" installiert mit --profile minimal, das clippy nicht mitbringt. Behoben durch rustup component add clippy direkt nach der Toolchain-Installation (siehe oben); bei einem aehnlichen Fehlerbild in Zukunft pruefen, ob diese Zeile noch vorhanden ist.
  2. apt-Paketname unbekannt / pkg-config findet eine Bibliothek nicht -- Paketnamen aendern sich gelegentlich zwischen Ubuntu-Versionen des Runner-Abbilds; den fehlenden .pc/.so-Namen aus der Fehlermeldung in apt-cache search nachschlagen und die Paketliste im Schritt "Systemabhaengigkeiten" ergaenzen.
  3. openssl-sys scheitert beim Windows-Cross-Ziel -- OpenSSL laesst sich fuer x86_64-pc-windows-msvc von Linux aus nicht ohne Weiteres cross-kompilieren; falls eine neue Abhaengigkeit das ueber openssl-sys statt rustls-tls einzieht, das Feature/die Abhaengigkeit auf rustls-tls umstellen (in diesem Job bislang nicht aufgetreten, reqwest ist bereits auf rustls-tls konfiguriert).
  4. NSIS-Plugin-Download schlaegt fehl -- Tauris NSIS-Bundler laedt beim ersten Bau zusaetzliche Plugins nach ~/.local/share/tauri; ein Netzwerkfehler dort bricht den Bauschritt "Windows-Installer bauen (Cross-Bau)" ab. Lauf erneut anstossen; bleibt der Cache warm, entfaellt der Download beim naechsten Mal.
  5. Runner-Speicher/-Zeit reicht nicht -- der Rust-Bau laeuft auf einem begrenzten Runner (8 Kerne/15 GB, siehe 18-CONTEXT.md); bei Speicherdruck CARGO_BUILD_JOBS (z. B. auf 4) als Umgebungsvariable im Job setzen, um die parallele Uebersetzung zu drosseln.

Job desktop: "A public key has been found, but no private key"

Die Tauri-CLI bricht den Bau ab, weil plugins.updater.pubkey in tauri.conf.json gesetzt ist, aber TAURI_SIGNING_PRIVATE_KEY in der Umgebung fehlt. Ursache sind fast immer fehlende oder umbenannte Secrets: in Gitea unter Repository > Settings > Actions > Secrets pruefen, ob TAURI_SIGNING_PRIVATE_KEY und TAURI_SIGNING_PRIVATE_KEY_PASSWORD unter genau diesen Namen existieren, und ob beide Bau-Schritte in ci.yml den env-Block tragen. Niemals --no-sign in ci.yml eintragen -- damit entstuenden unsignierte Pakete, die kein Client als Update annimmt (desktop-collect.sh bricht auf main/Tags ohne .sig ohnehin ab).

desktop baut, obwohl nichts geaendert wurde -- oder uebernimmt trotz Aenderung

Baut trotzdem:

  1. Erster Lauf nach einer Aenderung unter den Desktop-Pfaden -- der Stempel ist neu, das ist erwartet.
  2. Ein neuer Freigabe-Tag ist gesetzt -- die Version im Stempel hat sich geaendert, die Beta-Pakete muessen einmal neu entstehen.
  3. Der Eintrag ist vom Runner-Cache weggeraeumt worden -- act_runner raeumt ungenutzte Eintraege nach einigen Tagen, aeltere nach etwa einem Monat weg.
  4. ci.yml oder eines der Desktop-Skripte wurde geaendert -- beides gehoert selbst zur Pfadliste DESKTOP_PATHS.
  5. desktop-stamp.sh check hat den gefundenen Eintrag verworfen -- der Grund steht im Log des Schritts "Gefundene Pakete pruefen".

Uebernimmt trotz Aenderung: Die Aenderung liegt ausserhalb der Pfadliste (zum Beispiel nur pnpm-lock.yaml). Entweder zusaetzlich etwas unter apps/desktop/ aendern, oder DESKTOP_PATHS in .gitea/scripts/desktop-stamp.sh um den betroffenen Pfad erweitern -- diese Erweiterung loest selbst einen Neubau aus, weil desktop-stamp.sh Teil der eigenen Pfadliste ist.

publish: cache miss

actions/cache/restore@v4 mit fail-on-cache-miss: true bricht den Job publish hart ab, wenn kein Eintrag unter dem Schluessel desktop-dist-${{ gitea.sha }} existiert. Wahrscheinlichste Ursachen:

  1. Der Job desktop ist fehlgeschlagen oder uebersprungen worden (siehe if-Bedingung oben) -- im Gitea-Actions-Lauf pruefen, ob desktop tatsaechlich gruen war.
  2. Der Zwischenspeicher-Server des act_runner ist nicht erreichbar oder nicht aktiviert -- Runner-Konfiguration pruefen (Abschnitt 2, [cache] enabled muss gesetzt sein).
  3. Der Commit-SHA im Schluessel weicht zwischen den Jobs ab (sollte bei gitea.sha innerhalb desselben Laufs nicht vorkommen) -- bei Verdacht die Job-Logs beider Schritte (Uebergabe an publish in desktop, Desktop-Pakete aus dem Zwischenspeicher holen in publish) auf den verwendeten Schluessel vergleichen.

Release-Upload 413

Bricht der Datei-Upload in publish-release.sh mit HTTP 413 oder curl: (92) HTTP/2 ... PROTOCOL_ERROR ab, laeuft er ueber den Proxy vor git.vicolab.de -- genau das ist am 2026-09-17 bei Release 1.2.0 passiert (AppImage, 82 MB). Seitdem geht das Skript von selbst ueber das Host-Gateway (siehe Abschnitt 4); der Fehler kann nur noch auftreten, wenn GITEA_API ausdruecklich auf die oeffentliche Adresse gesetzt wird. Fehlende Anhaenge lassen sich jederzeit vom Host nachtragen: GITEA_TOKEN=... DESKTOP_DIST=<Ordner mit manifest.json> sh .gitea/scripts/publish-release.sh --tag vX.Y.Z (die Pakete liegen im API-Abbild unter /app/desktop-dist).

Job security ist rot oder gelb

Der Job ist ein reiner Bericht und beeinflusst weder Abbilder noch Freigaben (er laeuft erst nach publish und steht in keinem needs). Rot oder gelb heisst deshalb nie, dass etwas nicht ausgeliefert wurde. Zur Ursache im Job-Protokoll die Zeilen SECURITY-SUMMARY <werkzeug> skipped reason=... lesen: download oder checksum (Werkzeug nicht ladbar bzw. Summe stimmt nicht -- Netz pruefen, nie die Summe ohne Pruefung austauschen), kein-docker / install (Semgrep: Docker fehlt bzw. das Abbild liess sich nicht laden), kein-pnpm, abbild-fehlt (das Abbild liegt nicht im Docker-Daemon des Hosts), zeitgrenze (das Werkzeug hat sein eigenes Limit gerissen), gesamtbudget (die 25 Minuten waren aufgebraucht), fehler (Werkzeug ist abgestuerzt oder hat nichts geschrieben), kein-bericht. Ein haengender Lauf endet nach 30 Minuten von selbst. Fehlt nur das Artefakt, ist das erwartbar (best effort); die Zahlen stehen trotzdem im Protokoll.