From beab66648c08baa966fed7c3b2f13044d6cd6f68 Mon Sep 17 00:00:00 2001 From: tomdebone Date: Thu, 10 Sep 2026 22:58:25 +0200 Subject: [PATCH] =?UTF-8?q?ci:=20Release-Workflow=20f=C3=BCr=20den=20Deskt?= =?UTF-8?q?op-Client?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Das Repo hatte keine CI. Der Workflow folgt dem Muster, das im Nachbarprojekt lserver produktiv läuft: Tag v*.*.* baut, lädt die Bundles als Artefakte hoch und hängt sie an ein Gitea-Release. Zwei Jobs. Windows läuft auf dem Runner-Label `windows` (VM 131 winbuild) und liefert MSI und NSIS. Linux läuft bewusst NICHT auf `ubuntu-latest`: auf diesem Runner ist das Label auf docker://node:22-bookworm gemappt, also einen Container mit Node, aber ohne Rust und ohne GTK — der Job wäre bei cargo abgebrochen. Nachgesehen in /var/lib/gitea-runner/.runner. Das Host-Label heißt linux-amd64 und hat die vollständige Toolchain. macOS hat keinen Runner; der Weg für lokale .dmg-Builds steht als Kommentar im Workflow und in docs/tauri-release.md. AppImage ist ein eigener Schritt mit continue-on-error, weil es laut Infrastruktur-Doku an linuxdeploy/FUSE scheitert — deb und rpm sollen davon nicht mitgerissen werden. APPIMAGE_EXTRACT_AND_RUN und NO_STRIP sind gesetzt, patchelf war auf dem Runner nicht installiert und wurde nachgezogen. Vermerkt ist auch die Falle, die lserver einen Tag CI gekostet hat: sobald .gitea/workflows/ existiert, ignoriert Gitea .github/workflows/ vollständig — ohne roten Lauf, einfach still. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_013HC9HLrUU1LNwkzp8nkDLX --- .gitea/workflows/release.yml | 273 +++++++++++++++++++++++++++++++++++ docs/tauri-release.md | 50 ++++++- 2 files changed, 320 insertions(+), 3 deletions(-) create mode 100644 .gitea/workflows/release.yml diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml new file mode 100644 index 0000000..a519970 --- /dev/null +++ b/.gitea/workflows/release.yml @@ -0,0 +1,273 @@ +name: Release Desktop Client + +# Baut den Tauri-Client aus crates/tauri-app fuer Windows und Linux, laedt die +# Bundles als Job-Artefakte hoch und haengt sie an ein Gitea-Release zum Tag. +# +# ACHTUNG — Verzeichnis-Falle (hat im Nachbarprojekt lserver einen Tag CI +# gekostet): Sobald `.gitea/workflows/` existiert, ignoriert Gitea +# `.github/workflows/` KOMPLETT. In diesem Repo gibt es kein `.github/`, also +# ist heute nichts betroffen — wer aber spaeter einen Workflow unter +# `.github/workflows/` anlegt, bekommt keinen Lauf und auch keinen roten +# Fehler, sondern schlicht Stille. Alle Workflows gehoeren hierher. +# +# VOR DEM TAG VERSION BUMPEN (analog zu lserver, wo package.json gebumpt wird): +# Die Version im Release kommt aus dem Tag, die Version IM Artefaktnamen aus +# der Config. Beide muessen zusammenpassen, sonst heisst die Datei zu einem +# Tag v0.2.0 weiterhin `maarcadetweet_0.1.0_x64-setup.exe`: +# * crates/tauri-app/src-tauri/tauri.conf.json -> "version" +# * crates/tauri-app/src-tauri/Cargo.toml -> [package] version +# * crates/tauri-app/package.json -> "version" (Konsistenz) +# Details: docs/tauri-release.md, Abschnitt "Release-Checkliste". +# +# macOS: DAFUER GIBT ES KEINEN RUNNER. Weder Gitea-Instanz noch Infrastruktur +# haben einen macOS-Host; .dmg/.app werden lokal gebaut und von Hand an das +# hier erzeugte Release gehaengt: +# cd crates/tauri-app && npm ci && npm run tauri -- build --ci +# # Artefakte: src-tauri/target/release/bundle/dmg/*.dmg und macos/*.app +# # Universal-Build: npm run tauri -- build --ci --target universal-apple-darwin +# Danach im Gitea-Release "Edit release" -> Dateien anhaengen. Ohne +# Notarisierung meldet Gatekeeper die App als nicht verifiziert (siehe +# docs/tauri-release.md, Abschnitt 9 "Offene Punkte"). + +on: + push: + tags: + - "v*.*.*" + workflow_dispatch: + +jobs: + windows: + name: Windows (MSI + NSIS) + # Label `windows` = act_runner auf der Build-VM winbuild (192.168.1.69), + # Win11, cargo 1.98.1, Node 24, Tauri-CLI 2.11.4. Laut Infrastruktur-Doku + # on-demand — laeuft die VM nicht, wird der Job nie geplant (Gitea zeigt + # dann gar keinen Lauf an, keinen fehlgeschlagenen). + runs-on: windows + timeout-minutes: 60 + defaults: + run: + shell: powershell + steps: + - name: Checkout + uses: actions/checkout@v4 + + # package-lock.json liegt unter crates/tauri-app/ -> npm ci (reproduzierbar). + # Kein `npm install`: das wuerde den Lock im Build veraendern. + - name: Install frontend dependencies + working-directory: crates/tauri-app + run: npm ci + + # package.json definiert `"tauri": "tauri"` — der npm-Umweg nutzt die + # @tauri-apps/cli-devDependency aus dem Lock (2.x) statt einer global + # installierten `cargo tauri`-Version, ist also an das Repo gebunden. + # `cargo tauri build --ci` waere gleichwertig, haengt aber an dem, was + # gerade auf dem Runner installiert ist. + # bundle.targets in tauri.conf.json steht auf "all" -> unter Windows + # heisst das msi + nsis. + - name: Build Tauri bundles + working-directory: crates/tauri-app + run: npm run tauri -- build --ci + + - name: Bundles auflisten + working-directory: crates/tauri-app + run: | + $bundle = "src-tauri\target\release\bundle" + Get-ChildItem -Path $bundle -Recurse -Include *.msi, *.exe | + ForEach-Object { Write-Host "$($_.FullName) ($([math]::Round($_.Length / 1MB)) MB)" } + + - name: Upload bundles + uses: actions/upload-artifact@v4 + with: + name: maarcadetweet-windows + path: | + crates/tauri-app/src-tauri/target/release/bundle/msi/*.msi + crates/tauri-app/src-tauri/target/release/bundle/nsis/*.exe + retention-days: 30 + + - name: Gitea-Release anlegen und Bundles anhaengen + if: startsWith(github.ref, 'refs/tags/v') + env: + GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + $ErrorActionPreference = "Stop" + $api = "$env:GITHUB_SERVER_URL/api/v1/repos/$env:GITHUB_REPOSITORY" + $tag = $env:GITHUB_REF_NAME + $auth = "Authorization: token $env:GITEA_TOKEN" + + # Release-Notes aus dem passenden CHANGELOG-Abschnitt ziehen. + # CHANGELOG.md existiert in diesem Repo noch nicht — sobald es + # angelegt wird (Format `## [0.2.0] - ...` wie bei lserver), landet + # der Abschnitt automatisch im Release. + $notes = "Automatisch gebaut aus $env:GITHUB_SHA." + $version = $tag.TrimStart("v") + if (Test-Path CHANGELOG.md) { + # Ausdruecklich als UTF-8 lesen: Windows PowerShell 5.1 nimmt sonst + # die ANSI-Codepage und macht aus "Aenderungen" Buchstabensalat. + $lines = [IO.File]::ReadAllText((Resolve-Path CHANGELOG.md), [Text.UTF8Encoding]::new($false)) -split "`r?`n" + $start = ($lines | Select-String -Pattern "^## \[$([regex]::Escape($version))\]" | Select-Object -First 1) + if ($start) { + $from = $start.LineNumber + $rest = $lines[$from..($lines.Count - 1)] + $next = ($rest | Select-String -Pattern "^## \[" | Select-Object -First 1) + $take = if ($next) { $next.LineNumber - 2 } else { $rest.Count - 1 } + if ($take -ge 0) { $notes = ($rest[0..$take] -join "`n").Trim() } + } + } + + $body = @{ tag_name = $tag; name = "maarcadetweet $tag"; body = $notes } | ConvertTo-Json -Depth 3 + $bodyFile = Join-Path $env:RUNNER_TEMP "release.json" + [IO.File]::WriteAllText($bodyFile, $body, [Text.UTF8Encoding]::new($false)) + + # Beide Jobs (windows + linux) haengen an DASSELBE Release und laufen + # parallel. Deshalb: anlegen versuchen, und wenn das scheitert (der + # andere Job war schneller, oder es ist ein Re-Run), das vorhandene + # Release per Tag holen. + $created = curl.exe -s -X POST -H $auth -H "Content-Type: application/json" --data-binary "@$bodyFile" "$api/releases" | ConvertFrom-Json + if (-not $created.id) { + $created = curl.exe -s -H $auth "$api/releases/tags/$tag" | ConvertFrom-Json + } + if (-not $created.id) { throw "Konnte kein Release fuer $tag anlegen oder finden." } + + $bundle = "crates\tauri-app\src-tauri\target\release\bundle" + $files = Get-ChildItem -Path $bundle -Recurse -Include *.msi, *.exe + if (-not $files) { throw "Keine Windows-Bundles unter $bundle gefunden." } + foreach ($f in $files) { + $name = [Uri]::EscapeDataString($f.Name) + curl.exe -s -o NUL -w "Asset-Upload $($f.Name): HTTP %{http_code}`n" -X POST -H $auth -F "attachment=@$($f.FullName)" "$api/releases/$($created.id)/assets?name=$name" + } + Write-Host "Release: $env:GITHUB_SERVER_URL/$env:GITHUB_REPOSITORY/releases/tag/$tag" + + linux: + name: Linux (deb + rpm + AppImage) + # NICHT `ubuntu-latest` — auf diesem Runner (VM ci-runner, 192.168.1.72) + # ist dieses Label auf `docker://node:22-bookworm` gemappt, also einen + # Container mit Node, aber ohne Rust und ohne GTK. Der Job braeche dort + # bei `cargo` ab. Nachgesehen in /var/lib/gitea-runner/.runner: + # labels: ['ubuntu-latest:docker://node:22-bookworm', 'linux-amd64:host'] + # `linux-amd64` ist das Host-Label, und auf dem Host liegen Rust + # (/root/.cargo/bin, auch ohne Login-Shell im PATH), Node 22 und die + # Tauri-GTK-Deps. Der Runner-Dienst laeuft als root. + runs-on: linux-amd64 + timeout-minutes: 60 + defaults: + run: + shell: bash + env: + # linuxdeploy/appimagetool werden als AppImage aus ~/.cache/tauri + # gestartet und brauchen sonst FUSE, was in der VM nicht zuverlaessig + # funktioniert (siehe MAARCADE-INFRASTRUKTUR.md: "AppImage scheitert an + # linuxdeploy/FUSE"). Mit dieser Variable entpacken sie sich selbst. + APPIMAGE_EXTRACT_AND_RUN: "1" + # Das Release-Profil in src-tauri/Cargo.toml strippt bereits selbst; + # linuxdeploys eigener strip-Lauf ist dann nur eine weitere Fehlerquelle. + NO_STRIP: "true" + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Toolchain melden + run: | + node --version + npm --version + cargo --version || echo "cargo fehlt im PATH — ggf. ~/.cargo/env sourcen" + + - name: Install frontend dependencies + working-directory: crates/tauri-app + run: npm ci + + # bundle.targets in tauri.conf.json ist "all" -> unter Linux sind das + # deb, rpm und appimage. Bewusst in zwei Aufrufe getrennt: der + # AppImage-Schritt ist der fragile (Downloads von linuxdeploy + + # appimagetool beim ersten Lauf, FUSE), und wenn er faellt, sollen deb + # und rpm trotzdem im Release landen. Der zweite Aufruf ist billig — der + # Cargo-Release-Build ist dann schon im target/-Cache. + - name: Build Tauri bundles (deb + rpm) + working-directory: crates/tauri-app + run: npm run tauri -- build --ci --bundles deb,rpm + + - name: Build Tauri bundle (AppImage) + working-directory: crates/tauri-app + continue-on-error: true + run: npm run tauri -- build --ci --bundles appimage + + - name: Bundles auflisten + run: | + find crates/tauri-app/src-tauri/target/release/bundle \ + \( -name '*.deb' -o -name '*.rpm' -o -name '*.AppImage' \) \ + -printf '%p (%kK)\n' || true + + - name: Upload bundles + uses: actions/upload-artifact@v4 + with: + name: maarcadetweet-linux + path: | + crates/tauri-app/src-tauri/target/release/bundle/deb/*.deb + crates/tauri-app/src-tauri/target/release/bundle/rpm/*.rpm + crates/tauri-app/src-tauri/target/release/bundle/appimage/*.AppImage + # AppImage darf fehlen (siehe continue-on-error oben), deb/rpm nicht — + # ein komplett leerer Upload soll auffallen. + if-no-files-found: error + retention-days: 30 + + - name: Gitea-Release anlegen und Bundles anhaengen + if: startsWith(github.ref, 'refs/tags/v') + env: + GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + api="${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}" + tag="${GITHUB_REF_NAME}" + version="${tag#v}" + + # Gleiche CHANGELOG-Logik wie im Windows-Job. Die Datei existiert + # heute nicht; ohne sie bleibt es beim Fallback-Text. + notes="Automatisch gebaut aus ${GITHUB_SHA}." + if [ -f CHANGELOG.md ]; then + section="$(awk -v v="$version" ' + /^## \[/ { if (found) exit; if ($0 ~ "^## \\[" v "\\]") { found = 1; next } } + found { print } + ' CHANGELOG.md)" + if [ -n "$(printf '%s' "$section" | tr -d '[:space:]')" ]; then + notes="$section" + fi + fi + + # jq ist auf dem Runner nicht garantiert, Node 22 schon. + payload="${RUNNER_TEMP}/release.json" + NOTES="$notes" TAG="$tag" node -e ' + const fs = require("fs"); + fs.writeFileSync(process.argv[1], JSON.stringify({ + tag_name: process.env.TAG, + name: `maarcadetweet ${process.env.TAG}`, + body: process.env.NOTES, + })); + ' "$payload" + + # Anlegen oder — falls der Windows-Job schneller war bzw. das Release + # vom Re-Run schon existiert — das vorhandene holen. + id="$(curl -s -X POST -H "Authorization: token ${GITEA_TOKEN}" \ + -H "Content-Type: application/json" --data-binary "@${payload}" \ + "${api}/releases" | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{try{process.stdout.write(String(JSON.parse(s).id||""))}catch{}})')" + if [ -z "$id" ]; then + id="$(curl -s -H "Authorization: token ${GITEA_TOKEN}" \ + "${api}/releases/tags/${tag}" | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{try{process.stdout.write(String(JSON.parse(s).id||""))}catch{}})')" + fi + if [ -z "$id" ]; then + echo "Konnte kein Release fuer ${tag} anlegen oder finden." >&2 + exit 1 + fi + + bundle="crates/tauri-app/src-tauri/target/release/bundle" + found=0 + while IFS= read -r f; do + found=1 + name="$(basename "$f")" + code="$(curl -s -o /dev/null -w '%{http_code}' -X POST \ + -H "Authorization: token ${GITEA_TOKEN}" \ + -F "attachment=@${f}" \ + "${api}/releases/${id}/assets?name=${name}")" + echo "Asset-Upload ${name}: HTTP ${code}" + done < <(find "$bundle" \( -name '*.deb' -o -name '*.rpm' -o -name '*.AppImage' \) | sort) + [ "$found" -eq 1 ] || { echo "Keine Linux-Bundles unter ${bundle} gefunden." >&2; exit 1; } + + echo "Release: ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/releases/tag/${tag}" diff --git a/docs/tauri-release.md b/docs/tauri-release.md index 49b8407..350313b 100644 --- a/docs/tauri-release.md +++ b/docs/tauri-release.md @@ -296,13 +296,57 @@ beiden. 5. Mit einer älteren installierten Version gegenprüfen, dass `check()` das Update findet und die Signaturprüfung durchgeht. -## 8. Offene Punkte +## 8. CI: Tag-Release über Gitea Actions + +`.gitea/workflows/release.yml` baut Schritt 2 der Checkliste für **Windows und +Linux** automatisch. Trigger ist ein Tag `v*.*.*` (zusätzlich manuell per +`workflow_dispatch`, dann ohne Release-Anlage). + +| Job | `runs-on` | Runner | Bundles | +|---|---|---|---| +| `windows` | `windows` | winbuild, 192.168.1.69 (on-demand) | `msi/*.msi`, `nsis/*-setup.exe` | +| `linux` | `ubuntu-latest` | ci-runner, 192.168.1.72 | `deb/*.deb`, `rpm/*.rpm`, `appimage/*.AppImage` | + +Beide Jobs laufen `npm ci` (es gibt eine `package-lock.json`) und danach den +npm-Skript-Umweg `npm run tauri -- build --ci` aus `crates/tauri-app/`, damit +die im Lock gepinnte `@tauri-apps/cli` benutzt wird und nicht die zufällig auf +dem Runner installierte. Die Pfade sind die aus Abschnitt 5. Beide laden ihre +Bundles als Job-Artefakt hoch **und** hängen sie an dasselbe Gitea-Release zum +Tag (anlegen, und falls der andere Job schneller war, das vorhandene per Tag +holen). Die Release-Beschreibung kommt aus dem passenden +`## []`-Abschnitt einer `CHANGELOG.md`, sobald es eine gibt — bis +dahin steht dort der Commit-SHA. + +Zwei Dinge, die der Workflow *nicht* tut: + +* **macOS.** Es gibt keinen macOS-Runner. `.dmg`/`.app` werden lokal nach + Abschnitt 4 gebaut und im Gitea-Release von Hand angehängt. +* **Signierte Updater-Artefakte.** Der Workflow baut ohne Release-Overlay und + ohne `TAURI_SIGNING_PRIVATE_KEY*`; es entstehen also keine `.sig`-Dateien + (Abschnitt 2 und 5). Für ein echtes Auto-Update müssen Overlay-Datei und + Secrets ergänzt und der Build-Aufruf um + `--config src-tauri/tauri.release.conf.json` erweitert werden. + +**Vor dem Tag zu bumpen** (Schritt 1 der Checkliste): Die Release-Version kommt +aus dem Tag, die Version im *Dateinamen* aus `src-tauri/tauri.conf.json`. Ohne +Bump heißt das Artefakt zu `v0.2.0` weiterhin +`maarcadetweet_0.1.0_x64-setup.exe`. `src-tauri/Cargo.toml` und `package.json` +mitziehen — alle drei stehen aktuell auf `0.1.0`. + +> **Falle:** Sobald `.gitea/workflows/` existiert, ignoriert Gitea +> `.github/workflows/` vollständig — kommentarlos, ohne roten Lauf. Im +> Nachbarprojekt `lserver` waren Tests dadurch einen Tag lang still +> abgeschaltet. Dieses Repo hat kein `.github/`, und das soll so bleiben: neue +> Workflows gehören nach `.gitea/workflows/`. + +## 9. Offene Punkte * Kein Release-Overlay im Repo — die Datei aus Abschnitt 2 muss angelegt werden. Der Endpoint `https://releases.maarcadetweet.local/…` in der aktuellen Config ist ein Platzhalter und existiert nicht. -* Kein Update-Server, kein CI-Workflow, kein Skript, das `latest.json` erzeugt - (`scripts/` ist leer). +* Kein Update-Server und kein Skript, das `latest.json` erzeugt (`scripts/` ist + leer). Der CI-Workflow aus Abschnitt 8 baut und veröffentlicht Installer, + aber keine Updater-Artefakte. * Keine Code-Signierung/Notarisierung für macOS und keine Authenticode-Signatur für Windows konfiguriert (`bundle` enthält weder `macOS.signingIdentity` noch `windows.certificateThumbprint`). Der Tauri-Updater-Schlüssel ersetzt das