fix(publish): robustes Error-Handling in publish-apk.ps1

- Pre-Checks vor jeder Dateiänderung (Token, Regex-Validierung, SSH-Agent)
- Rollback: build.gradle.kts wird bei Build- oder Upload-Fehler zurückgesetzt
- SCP/SSH mit ConnectTimeout (kein ewiges Hängen bei VPS-Ausfall)
- API-Call mit Retry (2 Versuche, 3s Pause) + Recovery-Hinweis
- Verify + Git: non-fatal (nur Warnung, kein Abbruch)
- versionCode-Validierung: neuer Code muss > aktueller sein
- Set-StrictMode -Version Latest
This commit is contained in:
Jens Reinemann 2026-05-18 11:38:01 +02:00
parent f3eab7b10d
commit 23e0a47967

View file

@ -9,6 +9,11 @@
4. Server-Version per API aktualisieren (kein Container-Neustart nötig)
5. Verifizieren (überspringbar mit -SkipVerify)
6. git commit + push überspringbar mit -SkipPush
Rollback-Strategie:
- Fehler vor/während Upload build.gradle.kts wird automatisch zurückgesetzt
- Fehler nach Upload (API) APK liegt schon oben; kein Rollback, Recovery-Hinweis
- Fehler bei Verify/Git Deployment war erfolgreich; nur Warnung, kein Abbruch
.PARAMETER VersionName
Neue versionName (z.B. "1.8"). Wenn weggelassen, bleibt die aktuelle.
.PARAMETER VersionCode
@ -35,128 +40,253 @@ param(
[switch] $SkipPush
)
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
$VPS = "root@195.246.231.210"
$RemoteDir = "/opt/bollwerk"
$ServerUrl = "https://bollwerk.online"
$BuildGradle = "app/build.gradle.kts"
# --- Admin-Token laden ---
# ─────────────────────────────────────────────────────────────────────────────
# Hilfsfunktionen
# ─────────────────────────────────────────────────────────────────────────────
function Rollback-Gradle {
param([string]$OriginalContent)
Write-Host "[ROLLBACK] build.gradle.kts wird zurueckgesetzt..." -ForegroundColor DarkYellow
try {
[System.IO.File]::WriteAllText((Resolve-Path $BuildGradle).Path, $OriginalContent)
Write-Host "[ROLLBACK] build.gradle.kts zurueckgesetzt." -ForegroundColor DarkYellow
} catch {
Write-Warning "[ROLLBACK FEHLGESCHLAGEN] Bitte build.gradle.kts manuell korrigieren: $_"
}
}
function Write-Step {
param([string]$Text)
Write-Host "`n$Text" -ForegroundColor Yellow
}
function Write-OK { param([string]$Text); Write-Host "[OK] $Text" -ForegroundColor Green }
function Write-Skip { param([string]$Text); Write-Host "[--] $Text" -ForegroundColor DarkGray }
function Write-Fail { param([string]$Text); Write-Host "[!!] $Text" -ForegroundColor Red }
# ─────────────────────────────────────────────────────────────────────────────
# Pre-Checks (nichts wird verändert)
# ─────────────────────────────────────────────────────────────────────────────
# Admin-Token
$AdminToken = $env:BOLLWERK_ADMIN_TOKEN
if (-not $AdminToken) {
Write-Error "Umgebungsvariable BOLLWERK_ADMIN_TOKEN ist nicht gesetzt. Bitte setzen: `$env:BOLLWERK_ADMIN_TOKEN = 'dein-token'"
Write-Fail "BOLLWERK_ADMIN_TOKEN ist nicht gesetzt."
Write-Host " Bitte setzen: `$env:BOLLWERK_ADMIN_TOKEN = 'dein-token'" -ForegroundColor DarkGray
exit 1
}
# --- Version aus build.gradle.kts lesen und bumpen ---
$gradleContent = Get-Content $BuildGradle -Raw
$currentCode = [int]([regex]::Match($gradleContent, 'versionCode\s*=\s*(\d+)').Groups[1].Value)
$currentName = [regex]::Match($gradleContent, 'versionName\s*=\s*"([^"]+)"').Groups[1].Value
# build.gradle.kts lesen + Regex validieren
if (-not (Test-Path $BuildGradle)) {
Write-Fail "Datei nicht gefunden: $BuildGradle"
exit 1
}
$originalContent = Get-Content $BuildGradle -Raw
$codeMatch = [regex]::Match($originalContent, 'versionCode\s*=\s*(\d+)')
$nameMatch = [regex]::Match($originalContent, 'versionName\s*=\s*"([^"]+)"')
if (-not $codeMatch.Success) {
Write-Fail "versionCode nicht in $BuildGradle gefunden. Regex-Muster: 'versionCode = <int>'"
exit 1
}
if (-not $nameMatch.Success) {
Write-Fail "versionName nicht in $BuildGradle gefunden. Regex-Muster: versionName = `"<string>`""
exit 1
}
$currentCode = [int]$codeMatch.Groups[1].Value
$currentName = $nameMatch.Groups[1].Value
$newCode = if ($VersionCode -gt 0) { $VersionCode } else { $currentCode + 1 }
$newName = if ($VersionName) { $VersionName } else { $currentName }
if ($newCode -le $currentCode) {
Write-Fail "Neuer versionCode ($newCode) muss groesser als aktueller ($currentCode) sein."
exit 1
}
# SSH-Agent (vor jeder Dateiänderung prüfen)
$null = ssh-add -l 2>&1
if ($LASTEXITCODE -ne 0) {
Write-Fail "SSH-Agent hat keinen Key. Bitte ausfuehren: ssh-add C:\Users\JensR\.ssh\id_ed25519"
exit 1
}
Write-OK "Pre-Checks bestanden (Token, Gradle-Parsing, SSH-Agent)"
Write-Host ""
Write-Host "=== Publish APK v$newName (build $newCode) ===" -ForegroundColor Cyan
Write-Host " $currentName ($currentCode) → $newName ($newCode)" -ForegroundColor Gray
Write-Host ""
$updated = $gradleContent -replace "versionCode\s*=\s*$currentCode", "versionCode = $newCode"
# ─────────────────────────────────────────────────────────────────────────────
# Schritt 0: build.gradle.kts patchen
# ─────────────────────────────────────────────────────────────────────────────
$patchedContent = $originalContent -replace "versionCode\s*=\s*$currentCode\b", "versionCode = $newCode"
if ($VersionName) {
$updated = $updated -replace 'versionName\s*=\s*"[^"]+"', "versionName = `"$newName`""
}
[System.IO.File]::WriteAllText((Resolve-Path $BuildGradle).Path, $updated)
Write-Host "[OK] build.gradle.kts: versionCode=$newCode, versionName=$newName" -ForegroundColor Green
# --- Build ---
if ($SkipBuild) {
Write-Host "[--] Build uebersprungen (-SkipBuild)" -ForegroundColor DarkGray
} else {
Write-Host "`n[1/4] APK bauen..." -ForegroundColor Yellow
& ./gradlew assembleDebug
if ($LASTEXITCODE -ne 0) { Write-Error "Build fehlgeschlagen."; exit 1 }
Write-Host "[OK] APK gebaut" -ForegroundColor Green
}
if (-not (Test-Path $ApkPath)) {
Write-Error "APK nicht gefunden: $ApkPath - bitte zuerst './gradlew assembleDebug' ausfuehren."
exit 1
}
# SSH-Agent prüfen
$sshKeys = ssh-add -l 2>&1
if ($LASTEXITCODE -ne 0) {
Write-Error "SSH-Agent hat keinen Key geladen. Bitte 'ssh-add' ausfuehren."
exit 1
}
Write-Host "[OK] SSH-Agent aktiv" -ForegroundColor Green
# --- Schritt 2: APK hochladen ---
Write-Host "`n[2/4] APK hochladen..." -ForegroundColor Yellow
ssh $VPS "mkdir -p $RemoteDir/data"
scp $ApkPath "${VPS}:${RemoteDir}/data/app-latest.apk"
if ($LASTEXITCODE -ne 0) { Write-Error "APK-Upload fehlgeschlagen."; exit 1 }
Write-Host "[OK] APK hochgeladen" -ForegroundColor Green
# --- Schritt 3: Server über neue Version benachrichtigen ---
Write-Host "`n[3/4] Server-Version aktualisieren (API-Call)..." -ForegroundColor Yellow
$body = @{ versionCode = $newCode; versionName = $newName } | ConvertTo-Json -Compress
$headers = @{
"Authorization" = "Bearer $AdminToken"
"Content-Type" = "application/json"
$patchedContent = $patchedContent -replace 'versionName\s*=\s*"[^"]+"', "versionName = `"$newName`""
}
try {
$response = Invoke-WebRequest -Uri "$ServerUrl/api/admin/version" `
-Method POST `
-Headers $headers `
-Body $body `
-UseBasicParsing
if ($response.StatusCode -ne 200) {
Write-Error "Version-Update fehlgeschlagen: HTTP $($response.StatusCode)"
exit 1
}
[System.IO.File]::WriteAllText((Resolve-Path $BuildGradle).Path, $patchedContent)
Write-OK "build.gradle.kts aktualisiert (versionCode=$newCode, versionName=$newName)"
} catch {
Write-Error "Version-Update fehlgeschlagen: $_"
Write-Fail "build.gradle.kts konnte nicht geschrieben werden: $_"
exit 1
}
Write-Host "[OK] Version gesetzt: $newName (build $newCode)" -ForegroundColor Green
# --- Schritt 4: Verifizieren ---
if ($SkipVerify) {
Write-Host "`n[4/4] Verifizierung uebersprungen." -ForegroundColor DarkGray
# ─────────────────────────────────────────────────────────────────────────────
# Schritt 1: Build
# ─────────────────────────────────────────────────────────────────────────────
if ($SkipBuild) {
Write-Skip "Build uebersprungen (-SkipBuild)"
} else {
Write-Host "`n[4/4] Verifizieren..." -ForegroundColor Yellow
Write-Step "[1/4] APK bauen..."
try {
& ./gradlew assembleDebug
if ($LASTEXITCODE -ne 0) { throw "Gradle exit code $LASTEXITCODE" }
Write-OK "APK gebaut"
} catch {
Write-Fail "Build fehlgeschlagen: $_"
Rollback-Gradle $originalContent
exit 1
}
}
if (-not (Test-Path $ApkPath)) {
Write-Fail "APK nicht gefunden nach Build: $ApkPath"
Rollback-Gradle $originalContent
exit 1
}
# ─────────────────────────────────────────────────────────────────────────────
# Schritt 2: APK hochladen (Rollback bei Fehler)
# ─────────────────────────────────────────────────────────────────────────────
Write-Step "[2/4] APK hochladen → VPS..."
try {
# Remote-Verzeichnis sicherstellen
$mkdirOutput = ssh -o ConnectTimeout=15 $VPS "mkdir -p $RemoteDir/data" 2>&1
if ($LASTEXITCODE -ne 0) { throw "ssh mkdir fehlgeschlagen (Exit $LASTEXITCODE): $mkdirOutput" }
# SCP mit Timeout-Hint (ConnectTimeout via SSH-Config oder ServerAliveInterval)
scp -o ConnectTimeout=30 $ApkPath "${VPS}:${RemoteDir}/data/app-latest.apk"
if ($LASTEXITCODE -ne 0) { throw "scp fehlgeschlagen (Exit $LASTEXITCODE)" }
Write-OK "APK hochgeladen"
} catch {
Write-Fail "Upload fehlgeschlagen: $_"
Rollback-Gradle $originalContent
Write-Host ""
Write-Host " TIPP: Pruefen ob VPS erreichbar ist:" -ForegroundColor DarkGray
Write-Host " ssh $VPS 'echo OK'" -ForegroundColor DarkGray
exit 1
}
# ─────────────────────────────────────────────────────────────────────────────
# Schritt 3: Server-Version per API setzen
# Ab hier kein Rollback mehr (APK liegt schon oben Recovery statt Rollback)
# ─────────────────────────────────────────────────────────────────────────────
Write-Step "[3/4] Server-Version aktualisieren..."
$apiBody = @{ versionCode = $newCode; versionName = $newName } | ConvertTo-Json -Compress
$apiHeaders = @{ "Authorization" = "Bearer $AdminToken"; "Content-Type" = "application/json" }
$apiSuccess = $false
$maxRetries = 2
for ($attempt = 1; $attempt -le $maxRetries; $attempt++) {
try {
$response = Invoke-WebRequest -Uri "$ServerUrl/api/admin/version" `
-Method POST -Headers $apiHeaders -Body $apiBody `
-UseBasicParsing -TimeoutSec 20
if ($response.StatusCode -eq 200) {
$apiSuccess = $true
break
}
throw "HTTP $($response.StatusCode)"
} catch {
if ($attempt -lt $maxRetries) {
Write-Host " Versuch $attempt fehlgeschlagen ($_) Retry in 3s..." -ForegroundColor DarkYellow
Start-Sleep -Seconds 3
} else {
Write-Fail "API-Call nach $maxRetries Versuchen fehlgeschlagen: $_"
Write-Host ""
Write-Host " APK liegt bereits auf dem VPS. Recovery-Befehl:" -ForegroundColor DarkYellow
Write-Host " Invoke-WebRequest -Uri '$ServerUrl/api/admin/version' ``" -ForegroundColor DarkGray
Write-Host " -Method POST -UseBasicParsing ``" -ForegroundColor DarkGray
Write-Host " -Headers @{ 'Authorization'='Bearer <TOKEN>'; 'Content-Type'='application/json' } ``" -ForegroundColor DarkGray
Write-Host " -Body '$apiBody'" -ForegroundColor DarkGray
}
}
}
if ($apiSuccess) {
Write-OK "Version gesetzt: $newName (build $newCode)"
} else {
# Deployment war teilweise erfolgreich → trotzdem committen, aber warnen
Write-Host ""
Write-Host " WARNUNG: VPS-Version nicht aktualisiert. Build.gradle.kts wird trotzdem committet." -ForegroundColor DarkYellow
}
# ─────────────────────────────────────────────────────────────────────────────
# Schritt 4: Verifizieren (non-fatal)
# ─────────────────────────────────────────────────────────────────────────────
if (-not $apiSuccess -or $SkipVerify) {
Write-Skip "Verifizierung uebersprungen"
} else {
Write-Step "[4/4] Verifizieren..."
Start-Sleep -Seconds 2
$versionResponse = Invoke-WebRequest -Uri "$ServerUrl/api/version" -UseBasicParsing
$versionJson = $versionResponse.Content | ConvertFrom-Json
try {
$versionJson = (Invoke-WebRequest -Uri "$ServerUrl/api/version" -UseBasicParsing -TimeoutSec 15).Content | ConvertFrom-Json
if ($versionJson.versionCode -eq $newCode -and $versionJson.versionName -eq $newName) {
Write-Host "[OK] /api/version: versionCode=$($versionJson.versionCode), versionName=$($versionJson.versionName)" -ForegroundColor Green
Write-OK "/api/version: $($versionJson.versionName) ($($versionJson.versionCode))"
} else {
Write-Warning "Version stimmt nicht ueberein! Erwartet: $newCode/$newName, Erhalten: $($versionJson.versionCode)/$($versionJson.versionName)"
Write-Host "[??] /api/version meldet $($versionJson.versionName) ($($versionJson.versionCode)) erwartet $newName ($newCode)" -ForegroundColor DarkYellow
}
} catch {
Write-Host "[??] /api/version nicht erreichbar: $_" -ForegroundColor DarkYellow
}
$homepageResponse = Invoke-WebRequest -Uri "$ServerUrl/" -UseBasicParsing
if ($homepageResponse.StatusCode -eq 200) {
Write-Host "[OK] Homepage erreichbar (HTTP $($homepageResponse.StatusCode))" -ForegroundColor Green
} else {
Write-Warning "Homepage liefert HTTP $($homepageResponse.StatusCode)"
try {
$hp = Invoke-WebRequest -Uri "$ServerUrl/" -UseBasicParsing -TimeoutSec 10
Write-OK "Homepage erreichbar (HTTP $($hp.StatusCode))"
} catch {
Write-Host "[??] Homepage nicht erreichbar: $_" -ForegroundColor DarkYellow
}
}
# --- Git Commit + Push ---
Write-Host "`n[Git] Committen..." -ForegroundColor Yellow
git add $BuildGradle
git commit -m "chore: release v$newName ($newCode)"
# ─────────────────────────────────────────────────────────────────────────────
# Git Commit + Push (non-fatal Deployment war erfolgreich)
# ─────────────────────────────────────────────────────────────────────────────
Write-Step "[Git] Version-Bump committen..."
try {
git add $BuildGradle
git commit -m "chore: release v$newName ($newCode)"
} catch {
Write-Host "[??] git commit fehlgeschlagen: $_" -ForegroundColor DarkYellow
Write-Host " Manuell: git add $BuildGradle ; git commit -m 'chore: release v$newName ($newCode)'" -ForegroundColor DarkGray
}
if (-not $SkipPush) {
try {
git push
Write-Host "[OK] Gepusht" -ForegroundColor Green
Write-OK "Gepusht"
} catch {
Write-Host "[??] git push fehlgeschlagen: $_" -ForegroundColor DarkYellow
Write-Host " Manuell: git push" -ForegroundColor DarkGray
}
} else {
Write-Host "[--] Push uebersprungen (-SkipPush)" -ForegroundColor DarkGray
Write-Skip "Push uebersprungen (-SkipPush)"
}
Write-Host "`n=== Publish abgeschlossen ===" -ForegroundColor Cyan
Write-Host "Homepage: $ServerUrl/" -ForegroundColor DarkGray
Write-Host "API: $ServerUrl/api/version" -ForegroundColor DarkGray
# ─────────────────────────────────────────────────────────────────────────────
# Zusammenfassung
# ─────────────────────────────────────────────────────────────────────────────
Write-Host ""
Write-Host "=== Publish abgeschlossen ===" -ForegroundColor Cyan
Write-Host " Version : $newName (build $newCode)"
Write-Host " Homepage: $ServerUrl/"
Write-Host " API : $ServerUrl/api/version"