507556f158
publish-release.sh ignoriert GITHUB_API_URL/GITHUB_SERVER_URL (git.vicolab.de hinter dem Proxy bricht grosse Uploads ab -- Release 1.2.0 blieb dadurch zunaechst ohne Anhaenge). Im CI wird das Host-Gateway des Job-Containers aus /proc/net/route ermittelt und Gitea direkt auf Port 3002 angesprochen, lokal localhost:3002; GITEA_API bleibt als Override. Gateway-Weg aus Job-Netz gegen die Gitea-API verifiziert; Anhaenge fuer v1.2.0 vom Host nachgetragen. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
352 lines
16 KiB
Markdown
352 lines
16 KiB
Markdown
# 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://<gitea-url>/<user>/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=<token-aus-schritt-oben>
|
|
```
|
|
|
|
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:
|
|
|
|
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).
|
|
|
|
**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:
|
|
|
|
```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.
|
|
|
|
### `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`).
|