# 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: ```bash git remote add origin https:////tessera-ctl.git ``` 3. 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): ```bash export GITEA_INSTANCE_URL=https://git.vicolab.de export GITEA_RUNNER_REGISTRATION_TOKEN= ``` Runner starten: ```bash docker compose -f docker-compose.ci.yml up -d ``` Runner-Status pruefen: ```bash 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: ```bash 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 wird genau eines: | 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. | 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 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 vier aufeinander aufbauenden Jobs: 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 Ablauf: `quality` -> `test` -> `desktop` -> `publish` (jeder Job nur bei Erfolg des vorherigen; `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 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-` -- 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-`) 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`). 6. **Pakete einsammeln** (`desktop-collect.sh --require linux,windows`) -- schreibt `manifest.json` und schlaegt fehl, wenn eine der beiden Dateien 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://: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: ```bash 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. ## 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. ### `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= sh .gitea/scripts/publish-release.sh --tag vX.Y.Z` (die Pakete liegen im API-Abbild unter `/app/desktop-dist`).