feat(18-01): Task 1 — Linux-Paket bis zum Download aus der API (Durchstich)

Die duenne Strecke Skript -> Abbild -> API beweisen (D-08, D-10):

- desktop-collect.sh sammelt AppImage/exe ein, schreibt manifest.json
  (Version, Kanal, Commit, Groesse, SHA-256) ohne Secrets
- apps/api/src/desktop/: neues Modul mit GET /desktop/latest und
  GET /desktop/download/:platform, beide @Public(); Plattform-Whitelist
  vor jedem Dateisystemzugriff, Dateiname ausschliesslich aus dem
  Manifest (T-18-01, T-18-02)
- packages/shared: DesktopPlatform/-Manifest(File)/-Latest(Response)
  Typen
- Dockerfile kopiert desktop-dist/ in die runner-Stufe; desktop-dist/
  per .gitkeep + .gitignore versioniert (leeres Verzeichnis, Pakete
  bleiben ungetrackt)
- HTTP-Durchstich-Spec (8 Tests) via NestFactory, kein fs-Mock; echtes
  Temp-Verzeichnis + unabhaengig berechneter SHA-256

Abweichung: DesktopController braucht @Inject(DesktopService) explizit
— Vitest transpiliert ueber esbuild, das emitDecoratorMetadata nicht
abbildet, sonst bleibt desktopService bei einem echten NestFactory-Bau
undefined (Rule 3, Blocker).

Lokal bewiesen: neu gebautes API-Abbild liefert /desktop/latest (200)
und /desktop/download/linux (200, attachment) aus, auch ueber
/api-proxy/ des Web-Containers.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-16 16:05:55 +02:00
parent 0e4eb9bf9e
commit ae8fecb538
10 changed files with 539 additions and 0 deletions
+156
View File
@@ -0,0 +1,156 @@
#!/bin/sh
# desktop-collect.sh -- Desktop-Pakete aus dem Tauri-Bau einsammeln, unter
# kanonischem Namen ablegen und manifest.json schreiben (Phase 18, D-08).
#
# Kanalmodell (identisch zu publish-images.sh, an GITHUB_REF entschieden,
# damit lokale Proben ohne Runner pruefbar sind):
# refs/tags/v* -> Kanal live, kein Namenssuffix
# refs/heads/main -> Kanal beta, Suffix -beta.{7-stelliger SHA} am Dateinamen
# alles andere -> Kanal dev, kein Suffix (lokale Proben tragen den
# Freigabe-Namen, damit desktop-version.sh/desktop-collect.sh
# ohne Pipeline durchgespielt werden koennen)
#
# Aufruf: sh .gitea/scripts/desktop-collect.sh --require linux[,windows]
#
# Umgebung:
# GITHUB_REF Kanalentscheidung (siehe oben)
# DESKTOP_DIST Zielordner fuer die Pakete (Vorgabe: desktop-dist)
# TAURI_DIR Tauri-Projektordner (Vorgabe: apps/desktop/src-tauri)
#
# Die Version kommt aus tauri.conf.json (von desktop-version.sh geschrieben
# oder als eingecheckte Basislinie vorhanden) -- dieses Skript liest sie nur,
# es schreibt sie nicht. Dieses Skript kennt kein Secret.
set -eu
REQUIRE=""
while [ $# -gt 0 ]; do
case "$1" in
--require)
shift
REQUIRE="${1:-}"
;;
*)
echo "Unbekanntes Argument: $1" >&2
exit 1
;;
esac
shift
done
if [ -z "$REQUIRE" ]; then
echo "Aufruf: $0 --require linux[,windows]" >&2
exit 1
fi
DESKTOP_DIST="${DESKTOP_DIST:-desktop-dist}"
TAURI_DIR="${TAURI_DIR:-apps/desktop/src-tauri}"
REF="${GITHUB_REF:-}"
case "$REF" in
refs/tags/v*)
CHANNEL=live
SUFFIX=""
;;
refs/heads/main)
CHANNEL=beta
SHA_SHORT="$(git rev-parse --short=7 HEAD)"
SUFFIX="-beta.$SHA_SHORT"
;;
*)
CHANNEL=dev
SUFFIX=""
;;
esac
VERSION="$(jq -r .version "$TAURI_DIR/tauri.conf.json")"
case "$VERSION" in
[0-9]*.[0-9]*.[0-9]*)
if ! printf '%s' "$VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$'; then
echo "Version '$VERSION' aus $TAURI_DIR/tauri.conf.json ist nicht rein numerisch (X.Y.Z)." >&2
exit 1
fi
;;
*)
echo "Version '$VERSION' aus $TAURI_DIR/tauri.conf.json ist nicht rein numerisch (X.Y.Z)." >&2
exit 1
;;
esac
COMMIT="$(git rev-parse --short=7 HEAD)"
BUILD_TIME="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
mkdir -p "$DESKTOP_DIST"
# Alte Pakete/Manifest entfernen, Platzhalter (.gitkeep) bleibt erhalten.
rm -f "$DESKTOP_DIST"/*.AppImage "$DESKTOP_DIST"/*.exe "$DESKTOP_DIST/manifest.json"
MANIFEST_ARGS=""
TMP_MANIFEST="$(mktemp)"
trap 'rm -f "$TMP_MANIFEST"' EXIT INT TERM
LINUX_NAME=""
LINUX_SIZE=""
LINUX_SHA=""
WINDOWS_NAME=""
WINDOWS_SIZE=""
WINDOWS_SHA=""
case ",$REQUIRE," in
*,linux,*)
APPIMAGE_DIR="$TAURI_DIR/target/release/bundle/appimage"
APPIMAGE_COUNT="$(find "$APPIMAGE_DIR" -maxdepth 1 -name '*.AppImage' 2>/dev/null | wc -l | tr -d ' ')"
if [ "$APPIMAGE_COUNT" -ne 1 ]; then
echo "Erwartet genau eine .AppImage-Datei in $APPIMAGE_DIR, gefunden: $APPIMAGE_COUNT" >&2
exit 1
fi
APPIMAGE_SRC="$(find "$APPIMAGE_DIR" -maxdepth 1 -name '*.AppImage')"
LINUX_NAME="Tessera-${VERSION}${SUFFIX}.AppImage"
cp "$APPIMAGE_SRC" "$DESKTOP_DIST/$LINUX_NAME"
LINUX_SIZE="$(stat -c %s "$DESKTOP_DIST/$LINUX_NAME")"
LINUX_SHA="$(sha256sum "$DESKTOP_DIST/$LINUX_NAME" | cut -d' ' -f1)"
echo "linux: $LINUX_NAME (${LINUX_SIZE} Bytes, sha256 $LINUX_SHA)"
;;
esac
case ",$REQUIRE," in
*,windows,*)
NSIS_DIR="$TAURI_DIR/target/x86_64-pc-windows-msvc/release/bundle/nsis"
NSIS_COUNT="$(find "$NSIS_DIR" -maxdepth 1 -name '*.exe' 2>/dev/null | wc -l | tr -d ' ')"
if [ "$NSIS_COUNT" -ne 1 ]; then
echo "Erwartet genau eine .exe-Datei in $NSIS_DIR, gefunden: $NSIS_COUNT" >&2
exit 1
fi
NSIS_SRC="$(find "$NSIS_DIR" -maxdepth 1 -name '*.exe')"
WINDOWS_NAME="Tessera-Setup-${VERSION}${SUFFIX}.exe"
cp "$NSIS_SRC" "$DESKTOP_DIST/$WINDOWS_NAME"
WINDOWS_SIZE="$(stat -c %s "$DESKTOP_DIST/$WINDOWS_NAME")"
WINDOWS_SHA="$(sha256sum "$DESKTOP_DIST/$WINDOWS_NAME" | cut -d' ' -f1)"
echo "windows: $WINDOWS_NAME (${WINDOWS_SIZE} Bytes, sha256 $WINDOWS_SHA)"
;;
esac
# manifest.json ausschliesslich ueber jq -n mit --arg/--argjson bauen (kein
# manuelles String-Zusammenbauen von JSON).
jq -n \
--arg version "$VERSION" \
--arg channel "$CHANNEL" \
--arg commit "$COMMIT" \
--arg buildTime "$BUILD_TIME" \
--arg linuxName "$LINUX_NAME" \
--argjson linuxSize "${LINUX_SIZE:-null}" \
--arg linuxSha "$LINUX_SHA" \
--arg windowsName "$WINDOWS_NAME" \
--argjson windowsSize "${WINDOWS_SIZE:-null}" \
--arg windowsSha "$WINDOWS_SHA" \
'{
version: $version,
channel: $channel,
commit: $commit,
buildTime: $buildTime,
files: (
{}
+ (if $linuxName != "" then { linux: { name: $linuxName, size: $linuxSize, sha256: $linuxSha } } else {} end)
+ (if $windowsName != "" then { windows: { name: $windowsName, size: $windowsSize, sha256: $windowsSha } } else {} end)
)
}' > "$DESKTOP_DIST/manifest.json"
echo "Manifest geschrieben: $DESKTOP_DIST/manifest.json (Version $VERSION, Kanal $CHANNEL)"