ci: Release-Workflow für den Desktop-Client
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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013HC9HLrUU1LNwkzp8nkDLX
This commit is contained in:
co-authored by
Claude Opus 5
parent
73959f9dde
commit
beab66648c
@@ -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}"
|
||||
+47
-3
@@ -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
|
||||
`## [<version>]`-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
|
||||
|
||||
Reference in New Issue
Block a user