# publish_update.ps1 - Upload Installer + Manifest nach Hetzner # Laedt in release/ UND release/downloads/ hoch (beide Pfade). # Aktualisiert zusaetzlich das Stable-Manifest fuer den Auto-Updater. # # ROOT CAUSE v1.3.0 (2026-05-31): # Der oeffentliche Installer war korrekt, aber das Stable-Manifest unter # /root/aza-app/release/downloads/updates/manifest.json zeigte noch die # alte Version, weil dieses Skript das Manifest dort nicht mitaktualisiert # hatte. Der Auto-Updater las das Stable-Manifest und bot kein Update an. # Fix: [5/7] aktualisiert das Stable-Manifest zwingend bei jedem Release. $ErrorActionPreference = "Stop" $projectRoot = $PSScriptRoot $remoteHost = "root@178.104.51.177" $remotePath = "/root/aza-app/release" $remoteDownloadsPath = "/root/aza-app/release/downloads" $remoteStableManifest = "/root/aza-app/release/downloads/updates/manifest.json" $publicInstallerUrl = "https://api.aza-medwork.ch/downloads/aza_desktop_setup.exe" $publicStableManifestUrl = "https://api.aza-medwork.ch/downloads/updates/manifest.json" $installerPath = Join-Path $projectRoot "dist\installer\aza_desktop_setup.exe" $manifestPath = Join-Path $projectRoot "release\version.json" Write-Host "" Write-Host "===================================================" Write-Host " AZA Release Upload + Verifikation" Write-Host "===================================================" Write-Host "" # --- [1/7] Lokale Dateien + Version lesen --- Write-Host "[1/7] Lokale Dateien pruefen + Version lesen..." if (-not (Test-Path $installerPath)) { Write-Error "ABBRUCH: $installerPath nicht gefunden." exit 1 } if (-not (Test-Path $manifestPath)) { Write-Error "ABBRUCH: $manifestPath nicht gefunden." exit 1 } if (-not (Test-Path (Join-Path $projectRoot "aza_version.py"))) { Write-Error "ABBRUCH: aza_version.py nicht gefunden." exit 1 } # Version + Build aus aza_version.py und _build_info.py lesen $versionContent = Get-Content (Join-Path $projectRoot "aza_version.py") -Raw if ($versionContent -match 'APP_VERSION\s*=\s*"([^"]+)"') { $appVersion = $matches[1].Trim() } else { Write-Error "ABBRUCH: APP_VERSION nicht in aza_version.py." exit 1 } if ($versionContent -match 'APP_CHANNEL\s*=\s*"([^"]+)"') { $appChannel = $matches[1].Trim() } else { $appChannel = "stable" } $buildStamp = "" $buildInfoPath = Join-Path $projectRoot "_build_info.py" if (Test-Path $buildInfoPath) { $buildContent = Get-Content $buildInfoPath -Raw if ($buildContent -match 'BUILD_TIMESTAMP\s*=\s*"([^"]+)"') { $buildStamp = $matches[1].Trim() } } if (-not $buildStamp) { $buildStamp = (Get-Item $installerPath).LastWriteTime.ToString("yyyyMMdd_HHmmss") } # Lokaler Installer-Hash + Groesse (einmalig berechnen) $localHash = (Get-FileHash -Path $installerPath -Algorithm SHA256).Hash.ToLower() $localSizeBytes = (Get-Item $installerPath).Length $localSizeMB = [math]::Round($localSizeBytes / 1MB, 2) Write-Host " Installer: $installerPath ($localSizeMB MB)" Write-Host " Version: $appVersion" Write-Host " Build: $buildStamp" Write-Host " Channel: $appChannel" Write-Host " SHA256: $localHash" # --- [2/7] Installer hochladen --- Write-Host "" Write-Host "[2/7] Installer hochladen (release/ und release/downloads/)..." scp "$installerPath" "${remoteHost}:${remotePath}/aza_desktop_setup.exe" if ($LASTEXITCODE -ne 0) { Write-Error "ABBRUCH: Installer-Upload nach release/ fehlgeschlagen." exit 1 } Write-Host " release/ hochgeladen" ssh $remoteHost "cp ${remotePath}/aza_desktop_setup.exe ${remoteDownloadsPath}/aza_desktop_setup.exe" if ($LASTEXITCODE -ne 0) { Write-Error "ABBRUCH: Kopieren nach release/downloads/ fehlgeschlagen." exit 1 } Write-Host " release/downloads/ kopiert" # --- [3/7] version.json hochladen --- Write-Host "" Write-Host "[3/7] release/version.json hochladen..." scp "$manifestPath" "${remoteHost}:${remotePath}/version.json" if ($LASTEXITCODE -ne 0) { Write-Error "ABBRUCH: version.json-Upload fehlgeschlagen." exit 1 } Write-Host " version.json OK" # --- [4/7] Server-Installer SHA256 pruefen --- Write-Host "" Write-Host "[4/7] Server-SHA256-Abgleich (release/ vs. downloads/)..." $hashOutput = ssh $remoteHost "sha256sum ${remotePath}/aza_desktop_setup.exe ${remoteDownloadsPath}/aza_desktop_setup.exe 2>/dev/null" Write-Host $hashOutput $hashes = ($hashOutput -split "`n") | Where-Object { $_ -match "^[0-9a-f]{64}" } | ForEach-Object { ($_ -split "\s+")[0] } if ($hashes.Count -lt 2) { Write-Error "ABBRUCH: SHA256 konnte nicht fuer beide Server-Pfade gelesen werden." exit 1 } if ($hashes[0] -ne $hashes[1]) { Write-Error "ABBRUCH: SHA256 release/ != release/downloads/ -- Installer-Pfade sind inkonsistent!" exit 1 } if ($hashes[0].ToLower() -ne $localHash) { Write-Error "ABBRUCH: Server-SHA256 ($($hashes[0])) != lokaler SHA256 ($localHash) -- Upload korrumpiert!" exit 1 } Write-Host " SHA256 identisch: release/ = downloads/ = lokal OK" # --- [5/7] Stable-Manifest aktualisieren --- # PFLICHTSCHRITT -- verhindert Wiederholung des v1.3.0-Fehlers. # Das Stable-Manifest liegt in einem anderen Pfad als version.json: # /root/aza-app/release/downloads/updates/manifest.json # Es wird bei jedem Release zwingend auf die neue Version gesetzt. Write-Host "" Write-Host "[5/7] Stable-Manifest aktualisieren ($remoteStableManifest)..." $stableManifestContent = @" { "product": "AZA Desktop", "channel": "$appChannel", "latest_version": "$appVersion", "latest_build": "$buildStamp", "min_supported_version": "1.2.0", "mandatory": false, "release_date": "$(Get-Date -Format 'yyyy-MM-dd')", "notes_de": [ "AZA $appVersion" ], "files": [ { "name": "aza_desktop_setup.exe", "url": "$publicInstallerUrl", "sha256": "$localHash", "size_bytes": $localSizeBytes } ] } "@ # Temporaere Datei lokal schreiben, dann hochladen $tmpManifest = Join-Path $env:TEMP "aza_stable_manifest_tmp.json" # UTF8 ohne BOM (Out-File -Encoding utf8 erzeugt auf Windows einen BOM) [System.IO.File]::WriteAllText($tmpManifest, $stableManifestContent, [System.Text.UTF8Encoding]::new($false)) scp "$tmpManifest" "${remoteHost}:${remoteStableManifest}" if ($LASTEXITCODE -ne 0) { Remove-Item $tmpManifest -ErrorAction SilentlyContinue Write-Error "ABBRUCH: Stable-Manifest-Upload fehlgeschlagen." exit 1 } Remove-Item $tmpManifest -ErrorAction SilentlyContinue Write-Host " Stable-Manifest hochgeladen" # Gegenpruefen: Manifest-SHA256 auf Server muss mit lokalem Hash uebereinstimmen $manifestHashCheck = ssh $remoteHost "python3 -c `"import json; d=json.load(open('$remoteStableManifest')); print(d['files'][0]['sha256'])`"" $manifestHashCheck = ($manifestHashCheck -split "`n")[0].Trim().ToLower() if ($manifestHashCheck -ne $localHash) { Write-Error "ABBRUCH: Manifest SHA256 ($manifestHashCheck) != lokaler Installer SHA256 ($localHash)!" exit 1 } Write-Host " Manifest-SHA256 stimmt mit Installer ueberein" $manifestVersionCheck = ssh $remoteHost "python3 -c `"import json; d=json.load(open('$remoteStableManifest')); print(d['latest_version'])`"" $manifestVersionCheck = ($manifestVersionCheck -split "`n")[0].Trim() if ($manifestVersionCheck -ne $appVersion) { Write-Error "ABBRUCH: Manifest version ($manifestVersionCheck) != Release-Version ($appVersion)!" exit 1 } Write-Host " Manifest-Version korrekt: $manifestVersionCheck" # --- [6/7] Public-URL-Verifikation --- Write-Host "" Write-Host "[6/7] Public-URL-Verifikation..." # Public Installer $installerStatus = ssh $remoteHost "curl -sI $publicInstallerUrl | head -1" $installerStatus = ($installerStatus -split "`n")[0].Trim() Write-Host " Public Installer: $installerStatus" if ($installerStatus -notmatch "200") { Write-Error "ABBRUCH: Public Installer ist nicht erreichbar (Status: $installerStatus)!" exit 1 } # Public Stable-Manifest $stableManifestStatus = ssh $remoteHost "curl -sI $publicStableManifestUrl | head -1" $stableManifestStatus = ($stableManifestStatus -split "`n")[0].Trim() Write-Host " Public Manifest: $stableManifestStatus" if ($stableManifestStatus -notmatch "200") { Write-Error "ABBRUCH: Public Stable-Manifest ist nicht erreichbar (Status: $stableManifestStatus)!" exit 1 } # Public Manifest zeigt neue Version $publicVersion = ssh $remoteHost "curl -s $publicStableManifestUrl | python3 -c `"import json,sys; d=json.load(sys.stdin); print(d.get('latest_version',''))`"" 2>/dev/null $publicVersion = ($publicVersion -split "`n")[0].Trim() Write-Host " Public Manifest latest_version: $publicVersion" if ($publicVersion -ne $appVersion) { Write-Error "ABBRUCH: Public Manifest zeigt Version $publicVersion statt $appVersion!" exit 1 } # --- [7/7] Abschlussbericht --- Write-Host "" Write-Host "===================================================" Write-Host " RELEASE-ABSCHLUSSBERICHT" Write-Host "===================================================" Write-Host " Version: $appVersion" Write-Host " Build: $buildStamp" Write-Host " Channel: $appChannel" Write-Host "" Write-Host " Lokaler Installer SHA256: $localHash" $serverHash = $hashes[0].ToLower() Write-Host " Server release/ SHA256: $serverHash" $serverDlHash = $hashes[1].ToLower() Write-Host " Server downloads/ SHA256: $serverDlHash" Write-Host " Manifest SHA256: $manifestHashCheck" Write-Host "" if ($localHash -eq $serverHash -and $localHash -eq $serverDlHash -and $localHash -eq $manifestHashCheck) { Write-Host " SHA256 ALLE IDENTISCH" -ForegroundColor Green } else { Write-Error "ABBRUCH: SHA256-Divergenz im Abschlussbericht!" exit 1 } Write-Host "" Write-Host " Public Installer: $installerStatus" Write-Host " Public Manifest: $stableManifestStatus" Write-Host " Public Manifest Version: $publicVersion" Write-Host "" Write-Host " Auto-Updater sieht neue Version: JA" -ForegroundColor Green Write-Host "" Write-Host "===================================================" Write-Host " UPLOAD FERTIG" Write-Host "===================================================" Write-Host ""