bollwerk/.github/skills/publish/publish-apk.ps1
Jens Reinemann 23e0a47967 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
2026-05-18 11:38:01 +02:00

292 lines
14 KiB
PowerShell
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<#
.SYNOPSIS
Vollständiger Publish-Workflow: Version bumpen, bauen, auf VPS deployen, committen.
.DESCRIPTION
Führt alle Schritte des Release-Workflows aus:
1. versionCode automatisch erhöhen (aus build.gradle.kts)
2. APK bauen (./gradlew assembleDebug) überspringbar mit -SkipBuild
3. APK per SCP auf VPS hochladen
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
Expliziter versionCode. Wenn weggelassen, wird der aktuelle automatisch um 1 erhöht.
.PARAMETER ApkPath
Pfad zur APK-Datei. Default: app/build/outputs/apk/debug/app-debug.apk
.PARAMETER SkipBuild
Gradle-Build überspringen (wenn APK bereits gebaut).
.PARAMETER SkipVerify
Verifizierung nach dem Deploy überspringen.
.PARAMETER SkipPush
Git-Push überspringen (nur lokaler Commit).
.EXAMPLE
& ".github/skills/publish/publish-apk.ps1"
& ".github/skills/publish/publish-apk.ps1" -VersionName "2.0"
& ".github/skills/publish/publish-apk.ps1" -SkipBuild -SkipPush
#>
param(
[string] $VersionName,
[int] $VersionCode = 0,
[string] $ApkPath = "app/build/outputs/apk/debug/app-debug.apk",
[switch] $SkipBuild,
[switch] $SkipVerify,
[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"
# ─────────────────────────────────────────────────────────────────────────────
# 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-Fail "BOLLWERK_ADMIN_TOKEN ist nicht gesetzt."
Write-Host " Bitte setzen: `$env:BOLLWERK_ADMIN_TOKEN = 'dein-token'" -ForegroundColor DarkGray
exit 1
}
# 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
# ─────────────────────────────────────────────────────────────────────────────
# Schritt 0: build.gradle.kts patchen
# ─────────────────────────────────────────────────────────────────────────────
$patchedContent = $originalContent -replace "versionCode\s*=\s*$currentCode\b", "versionCode = $newCode"
if ($VersionName) {
$patchedContent = $patchedContent -replace 'versionName\s*=\s*"[^"]+"', "versionName = `"$newName`""
}
try {
[System.IO.File]::WriteAllText((Resolve-Path $BuildGradle).Path, $patchedContent)
Write-OK "build.gradle.kts aktualisiert (versionCode=$newCode, versionName=$newName)"
} catch {
Write-Fail "build.gradle.kts konnte nicht geschrieben werden: $_"
exit 1
}
# ─────────────────────────────────────────────────────────────────────────────
# Schritt 1: Build
# ─────────────────────────────────────────────────────────────────────────────
if ($SkipBuild) {
Write-Skip "Build uebersprungen (-SkipBuild)"
} else {
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
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-OK "/api/version: $($versionJson.versionName) ($($versionJson.versionCode))"
} else {
Write-Host "[??] /api/version meldet $($versionJson.versionName) ($($versionJson.versionCode)) erwartet $newName ($newCode)" -ForegroundColor DarkYellow
}
} catch {
Write-Host "[??] /api/version nicht erreichbar: $_" -ForegroundColor DarkYellow
}
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 (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-OK "Gepusht"
} catch {
Write-Host "[??] git push fehlgeschlagen: $_" -ForegroundColor DarkYellow
Write-Host " Manuell: git push" -ForegroundColor DarkGray
}
} else {
Write-Skip "Push uebersprungen (-SkipPush)"
}
# ─────────────────────────────────────────────────────────────────────────────
# 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"