update
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
# AZA Reverse Proxy + Static Files
|
||||
#
|
||||
# Production: DOMAIN env must point to this machine (DNS A-Record).
|
||||
# Caddy handles automatic HTTPS via Let's Encrypt.
|
||||
#
|
||||
# Local testing: http://localhost:8080
|
||||
{
|
||||
email {$ACME_EMAIL:admin@aza-medwork.ch}
|
||||
}
|
||||
|
||||
{$AZA_DOMAIN:localhost} {
|
||||
encode gzip zstd
|
||||
|
||||
# Static web assets (landing, download, styles)
|
||||
handle /web/* {
|
||||
root * /app
|
||||
file_server
|
||||
}
|
||||
|
||||
# Release artifacts (version.json, installer)
|
||||
handle /release/* {
|
||||
root * /app
|
||||
file_server
|
||||
}
|
||||
|
||||
# Download shortcut: /download/filename -> /release/filename
|
||||
handle /download/* {
|
||||
root * /app/release
|
||||
uri strip_prefix /download
|
||||
file_server
|
||||
}
|
||||
|
||||
handle /downloads/* {
|
||||
root * /app/release
|
||||
uri strip_prefix /downloads
|
||||
file_server
|
||||
}
|
||||
|
||||
# All other requests -> backend API
|
||||
handle {
|
||||
reverse_proxy {$BACKEND_UPSTREAM:backend:8000}
|
||||
}
|
||||
}
|
||||
|
||||
# Empfang-Subdomain: empfang.aza-medwork.ch
|
||||
# Root "/" wird transparent auf /empfang/ umgeschrieben (kein sichtbarer Redirect)
|
||||
{$AZA_EMPFANG_DOMAIN:empfang.aza-medwork.ch} {
|
||||
encode gzip zstd
|
||||
|
||||
handle / {
|
||||
rewrite * /empfang/
|
||||
reverse_proxy {$BACKEND_UPSTREAM:backend:8000}
|
||||
}
|
||||
|
||||
handle {
|
||||
reverse_proxy {$BACKEND_UPSTREAM:backend:8000}
|
||||
}
|
||||
}
|
||||
|
||||
:8080 {
|
||||
encode gzip zstd
|
||||
|
||||
handle /web/* {
|
||||
root * /app
|
||||
file_server
|
||||
}
|
||||
|
||||
handle /release/* {
|
||||
root * /app
|
||||
file_server
|
||||
}
|
||||
|
||||
handle /download/* {
|
||||
root * /app/release
|
||||
uri strip_prefix /download
|
||||
file_server
|
||||
}
|
||||
|
||||
handle /downloads/* {
|
||||
root * /app/release
|
||||
uri strip_prefix /downloads
|
||||
file_server
|
||||
}
|
||||
|
||||
handle {
|
||||
reverse_proxy {$BACKEND_UPSTREAM_LOCAL:localhost:8000}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
Backup vor AZA Update-System Phase 1 (v2 — Logging, required, Build-Tie-Break).
|
||||
|
||||
Wiederherstellen (PowerShell, Arbeitsverzeichnis: AzA march 2026):
|
||||
|
||||
$b = 'backup_aza_update_phase1_v2_20260515_233059'
|
||||
Copy-Item -LiteralPath "$b\desktop_update_check.py" -Destination .\desktop_update_check.py -Force
|
||||
Copy-Item -LiteralPath "$b\basis14.py" -Destination .\basis14.py -Force
|
||||
Copy-Item -LiteralPath "$b\build_release_manifest.ps1" -Destination .\build_release_manifest.ps1 -Force
|
||||
Copy-Item -LiteralPath "$b\version.json" -Destination .\release\version.json -Force
|
||||
Copy-Item -LiteralPath "$b\Caddyfile" -Destination .\deploy\Caddyfile -Force
|
||||
Copy-Item -LiteralPath "$b\Caddyfile" -Destination .\deploy\aza-deploy\Caddyfile -Force
|
||||
Copy-Item -LiteralPath "$b\publish_update.ps1" -Destination .\publish_update.ps1 -Force
|
||||
Copy-Item -LiteralPath "$b\release.ps1" -Destination .\release.ps1 -Force
|
||||
|
||||
Hinweis: Im Backup liegen zwei Kopien unter demselben Dateinamen Caddyfile;
|
||||
bei Restore den Inhalt von deploy\aza-deploy\Caddyfile manuell aus der zweiten
|
||||
Server-Kopie verifizieren (falls abweichend).
|
||||
17988
AzA march 2026/backup_aza_update_phase1_v2_20260515_233059/basis14.py
Normal file
17988
AzA march 2026/backup_aza_update_phase1_v2_20260515_233059/basis14.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,97 @@
|
||||
$projectRoot = $PSScriptRoot
|
||||
$azaVersionPy = Join-Path $projectRoot "aza_version.py"
|
||||
$manifestPath = Join-Path $projectRoot "release\version.json"
|
||||
$releaseBaseUrl = $env:AZA_RELEASE_BASE_URL
|
||||
$installerPath = Join-Path $projectRoot "dist\installer\aza_desktop_setup.exe"
|
||||
|
||||
if (-not (Test-Path $azaVersionPy)) {
|
||||
Write-Error "aza_version.py nicht gefunden: $azaVersionPy"
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host "Erzeuge release/version.json aus aza_version.py..."
|
||||
|
||||
$versionContent = Get-Content $azaVersionPy -Raw
|
||||
if ($versionContent -match 'APP_VERSION\s*=\s*"([^"]+)"') {
|
||||
$appVersion = $matches[1].Trim()
|
||||
} else {
|
||||
Write-Error "APP_VERSION nicht in aza_version.py gefunden."
|
||||
exit 1
|
||||
}
|
||||
if ($versionContent -match 'APP_CHANNEL\s*=\s*"([^"]+)"') {
|
||||
$appChannel = $matches[1].Trim()
|
||||
} else {
|
||||
Write-Error "APP_CHANNEL nicht in aza_version.py gefunden."
|
||||
exit 1
|
||||
}
|
||||
|
||||
$manifest = @{}
|
||||
if (Test-Path $manifestPath) {
|
||||
$manifest = Get-Content $manifestPath -Raw | ConvertFrom-Json
|
||||
}
|
||||
|
||||
$releaseDate = if ($manifest.release_date) { $manifest.release_date } else { (Get-Date).ToString("yyyy-MM-dd") }
|
||||
$minVersion = if ($manifest.minimum_supported_version) { $manifest.minimum_supported_version } else { $appVersion }
|
||||
$minRequired = if ($manifest.min_required_version) { $manifest.min_required_version } else { $minVersion }
|
||||
$downloadUrl = if ($releaseBaseUrl) {
|
||||
"$releaseBaseUrl/aza_desktop_setup.exe"
|
||||
} elseif ($manifest.download_url) {
|
||||
$manifest.download_url
|
||||
} else {
|
||||
"https://api.aza-medwork.ch/downloads/aza_desktop_setup.exe"
|
||||
}
|
||||
$installerType = if ($manifest.installer_type) { $manifest.installer_type } else { "inno-setup" }
|
||||
$notesExisting = @()
|
||||
if ($manifest.notes -and $manifest.notes.Length -gt 0) {
|
||||
$notesExisting = @($manifest.notes)
|
||||
} elseif ($manifest.release_notes -and $manifest.release_notes.Length -gt 0) {
|
||||
$notesExisting = @($manifest.release_notes)
|
||||
}
|
||||
$notesList = if ($notesExisting.Count -gt 0) { $notesExisting } else { @("Desktop-Build aktualisiert") }
|
||||
$updateLevel = if ($manifest.update_level) { $manifest.update_level.ToString().ToLowerInvariant() } else { "recommended" }
|
||||
if ($updateLevel -ne "optional" -and $updateLevel -ne "recommended") {
|
||||
$updateLevel = "recommended"
|
||||
}
|
||||
|
||||
$buildStamp = (Get-Date).ToString("yyyyMMdd_HHmmss")
|
||||
$sha256Hex = $null
|
||||
if (Test-Path $installerPath) {
|
||||
$buildStamp = (Get-Item $installerPath).LastWriteTime.ToString("yyyyMMdd_HHmmss")
|
||||
$sha256Hex = (Get-FileHash -Path $installerPath -Algorithm SHA256).Hash
|
||||
} elseif ($manifest.build) {
|
||||
$buildStamp = $manifest.build.ToString()
|
||||
}
|
||||
if (-not $sha256Hex -and $manifest.sha256) {
|
||||
$sha256Hex = $manifest.sha256.ToString()
|
||||
}
|
||||
|
||||
$newManifest = [ordered]@{
|
||||
version = $appVersion
|
||||
build = $buildStamp
|
||||
channel = $appChannel
|
||||
release_date = $releaseDate
|
||||
minimum_supported_version = $minVersion
|
||||
min_required_version = $minRequired
|
||||
download_url = $downloadUrl
|
||||
sha256 = $sha256Hex
|
||||
update_level = $updateLevel
|
||||
installer_type = $installerType
|
||||
notes = $notesList
|
||||
release_notes = $notesList
|
||||
}
|
||||
|
||||
$newManifest | ConvertTo-Json -Depth 6 | Set-Content $manifestPath -Encoding UTF8
|
||||
|
||||
if (-not $appVersion) {
|
||||
Write-Error "release/version.json konnte nicht erzeugt werden."
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host "release/version.json aktualisiert."
|
||||
Write-Host "Version: $appVersion Build: $buildStamp"
|
||||
if ($sha256Hex) {
|
||||
Write-Host "SHA256: $sha256Hex"
|
||||
}
|
||||
if ($releaseBaseUrl) {
|
||||
Write-Host "Download-URL Base: $releaseBaseUrl"
|
||||
}
|
||||
@@ -0,0 +1,286 @@
|
||||
import webbrowser
|
||||
|
||||
import requests
|
||||
from aza_version import APP_CHANNEL, APP_VERSION
|
||||
|
||||
# Mehrere Pfade: /download/ (bestehende Instanzen) und /release/ (direkt unter Dateibaum).
|
||||
UPDATE_MANIFEST_URLS = (
|
||||
"https://api.aza-medwork.ch/download/version.json",
|
||||
"https://api.aza-medwork.ch/release/version.json",
|
||||
)
|
||||
DEFAULT_DOWNLOAD_URL = "https://api.aza-medwork.ch/downloads/aza_desktop_setup.exe"
|
||||
|
||||
|
||||
def _parse_version_tuple(v: str) -> tuple:
|
||||
parts = []
|
||||
for p in str(v).strip().split("."):
|
||||
try:
|
||||
parts.append(int(p))
|
||||
except Exception:
|
||||
parts.append(0)
|
||||
while len(parts) < 3:
|
||||
parts.append(0)
|
||||
return tuple(parts[:3])
|
||||
|
||||
|
||||
def _is_newer_version(latest: str, current: str) -> bool:
|
||||
return _parse_version_tuple(latest) > _parse_version_tuple(current)
|
||||
|
||||
|
||||
def _below_min_required(current: str, min_req: str) -> bool:
|
||||
mr = (min_req or "").strip()
|
||||
if not mr:
|
||||
return False
|
||||
return _parse_version_tuple(current) < _parse_version_tuple(mr)
|
||||
|
||||
|
||||
def _normalize_notes(data: dict) -> list[str]:
|
||||
raw = data.get("notes")
|
||||
if raw is None:
|
||||
raw = data.get("release_notes")
|
||||
if raw is None:
|
||||
return []
|
||||
if isinstance(raw, str):
|
||||
return [raw.strip()] if raw.strip() else []
|
||||
if isinstance(raw, list):
|
||||
return [str(x).strip() for x in raw if str(x).strip()]
|
||||
return []
|
||||
|
||||
|
||||
def _min_required_from_manifest(data: dict) -> str:
|
||||
for key in ("min_required_version", "minimum_supported_version"):
|
||||
v = data.get(key)
|
||||
if v is not None and str(v).strip():
|
||||
return str(v).strip()
|
||||
return ""
|
||||
|
||||
|
||||
def fetch_remote_manifest() -> tuple[dict | None, str | None]:
|
||||
"""Laedt das Release-Manifest. Fehler -> (None, fehlertext)."""
|
||||
last_err = "Keine Antwort vom Update-Server"
|
||||
for url in UPDATE_MANIFEST_URLS:
|
||||
try:
|
||||
r = requests.get(url, timeout=6)
|
||||
if r.status_code != 200:
|
||||
last_err = f"HTTP {r.status_code} von {url}"
|
||||
continue
|
||||
data = r.json()
|
||||
if not isinstance(data, dict):
|
||||
last_err = "Ungueltiges Manifest"
|
||||
continue
|
||||
return data, None
|
||||
except Exception as e:
|
||||
last_err = str(e) or repr(e)
|
||||
return None, last_err
|
||||
|
||||
|
||||
def check_for_updates():
|
||||
"""Prueft, ob eine neuere Version angekuendigt ist. Keine Netzwerkmeldung bei Fehler."""
|
||||
data, err = fetch_remote_manifest()
|
||||
if err or not data:
|
||||
return None
|
||||
|
||||
channel = str(data.get("channel") or "stable").strip()
|
||||
if channel != APP_CHANNEL:
|
||||
return None
|
||||
|
||||
latest = str(data.get("version") or "").strip()
|
||||
if not latest:
|
||||
return None
|
||||
|
||||
min_req = _min_required_from_manifest(data)
|
||||
download_url = str(data.get("download_url") or "").strip() or DEFAULT_DOWNLOAD_URL
|
||||
update_level = str(data.get("update_level") or "recommended").strip().lower()
|
||||
if update_level not in ("optional", "recommended"):
|
||||
update_level = "recommended"
|
||||
|
||||
notes = _normalize_notes(data)
|
||||
remote_build = str(data.get("build") or "").strip()
|
||||
sha256 = str(data.get("sha256") or "").strip()
|
||||
|
||||
need_version_bump = _is_newer_version(latest, APP_VERSION)
|
||||
need_min = _below_min_required(APP_VERSION, min_req)
|
||||
|
||||
if not need_version_bump and not need_min:
|
||||
return None
|
||||
|
||||
return {
|
||||
"update_available": True,
|
||||
"latest_version": latest,
|
||||
"current_version": APP_VERSION,
|
||||
"download_url": download_url,
|
||||
"remote_build": remote_build,
|
||||
"sha256": sha256,
|
||||
"update_level": update_level,
|
||||
"min_required_version": min_req,
|
||||
"notes": notes,
|
||||
"below_min_required": bool(min_req) and need_min,
|
||||
}
|
||||
|
||||
|
||||
def _startup_should_show_dialog(info: dict) -> bool:
|
||||
"""Optionale Updates nur bei manueller Pruefung stoeren nicht den Start."""
|
||||
if info.get("below_min_required"):
|
||||
return True
|
||||
if info.get("update_level") == "optional":
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def _show_update_notification(info: dict, parent):
|
||||
"""In-App-Dialog: Hinweis und Button oeffnet den offiziellen Download im Browser."""
|
||||
import tkinter as tk
|
||||
from tkinter import ttk, scrolledtext
|
||||
|
||||
download_url = info.get("download_url") or DEFAULT_DOWNLOAD_URL
|
||||
latest = info.get("latest_version", "")
|
||||
level = info.get("update_level", "recommended")
|
||||
notes = info.get("notes") or []
|
||||
|
||||
root = parent
|
||||
owns_root = False
|
||||
if root is None:
|
||||
root = tk.Tk()
|
||||
root.withdraw()
|
||||
owns_root = True
|
||||
|
||||
dlg = tk.Toplevel(root)
|
||||
dlg.title("AZA — Aktualisierung")
|
||||
if not owns_root:
|
||||
dlg.transient(root)
|
||||
dlg.resizable(True, True)
|
||||
dlg.minsize(420, 280)
|
||||
|
||||
if level == "optional":
|
||||
head = (
|
||||
f"Optionales Update: AZA {latest} ist verfuegbar.\n"
|
||||
f"Ihre Version: {APP_VERSION}\n\n"
|
||||
"Ueber die Schaltflaeche oeffnen Sie den offiziellen Installer-Download im Browser. "
|
||||
"AZA schliesst sich dafuer nicht automatisch."
|
||||
)
|
||||
elif info.get("below_min_required"):
|
||||
head = (
|
||||
f"Update erforderlich: Ihre Angabe bzw. Server-Vorgabe liegt unter der Mindestversion.\n"
|
||||
f"Neueste angebotene Version: {latest}\n"
|
||||
f"Ihre Version: {APP_VERSION}\n\n"
|
||||
"Bitte installieren Sie die aktuelle AZA-Version ueber den offiziellen Installer."
|
||||
)
|
||||
else:
|
||||
head = (
|
||||
f"Empfohlenes Update: AZA {latest} ist verfuegbar.\n"
|
||||
f"Ihre Version: {APP_VERSION}\n\n"
|
||||
"Ueber die Schaltflaeche oeffnen Sie den offiziellen Installer-Download im Browser."
|
||||
)
|
||||
|
||||
frm = ttk.Frame(dlg, padding=12)
|
||||
frm.pack(fill="both", expand=True)
|
||||
|
||||
ttk.Label(frm, text=head, wraplength=520, justify="left").pack(anchor="w", pady=(0, 8))
|
||||
|
||||
if notes:
|
||||
ttk.Label(frm, text="Aenderungen:", font=("Segoe UI", 9, "bold")).pack(anchor="w")
|
||||
box = scrolledtext.ScrolledText(frm, height=8, wrap="word", font=("Segoe UI", 9))
|
||||
box.pack(fill="both", expand=True, pady=(4, 8))
|
||||
box.insert("1.0", "\n".join(f"• {n}" for n in notes))
|
||||
box.configure(state="disabled")
|
||||
|
||||
btn_row = ttk.Frame(frm)
|
||||
btn_row.pack(fill="x", pady=(8, 0))
|
||||
|
||||
def on_download():
|
||||
try:
|
||||
webbrowser.open(download_url)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def on_close():
|
||||
dlg.destroy()
|
||||
if owns_root:
|
||||
try:
|
||||
root.destroy()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
ttk.Button(btn_row, text="Installer herunterladen (Browser)", command=on_download).pack(
|
||||
side="left", padx=(0, 8)
|
||||
)
|
||||
ttk.Button(btn_row, text="Schliessen", command=on_close).pack(side="left")
|
||||
|
||||
dlg.protocol("WM_DELETE_WINDOW", on_close)
|
||||
dlg.update_idletasks()
|
||||
try:
|
||||
_px = root.winfo_x() if not owns_root else None
|
||||
_py = root.winfo_y() if not owns_root else None
|
||||
if _px is not None and _py is not None:
|
||||
dlg.geometry(
|
||||
f"+{_px + max(20, (root.winfo_width() - dlg.winfo_reqwidth()) // 2)}"
|
||||
f"+{_py + max(20, (root.winfo_height() - dlg.winfo_reqheight()) // 2)}"
|
||||
)
|
||||
else:
|
||||
sw = dlg.winfo_screenwidth()
|
||||
sh = dlg.winfo_screenheight()
|
||||
dlg.geometry(
|
||||
f"+{(sw - dlg.winfo_reqwidth()) // 2}+{(sh - dlg.winfo_reqheight()) // 2}"
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if owns_root:
|
||||
dlg.grab_set()
|
||||
dlg.wait_window()
|
||||
else:
|
||||
dlg.grab_set()
|
||||
parent.wait_window(dlg)
|
||||
|
||||
|
||||
def prompt_update_if_available():
|
||||
"""Beim Start: still pruefen; bei empfohlenem / Pflicht-Update In-App-Dialog (kein Auto-Download)."""
|
||||
info = check_for_updates()
|
||||
if not info or not info.get("update_available"):
|
||||
return
|
||||
if not _startup_should_show_dialog(info):
|
||||
return
|
||||
try:
|
||||
_show_update_notification(info, parent=None)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def manual_check_for_updates(parent=None):
|
||||
"""Explizite Pruefung in der App (z. B. Ueber-Dialog)."""
|
||||
from tkinter import messagebox
|
||||
|
||||
data, err = fetch_remote_manifest()
|
||||
if err:
|
||||
messagebox.showerror(
|
||||
"Nach Updates suchen",
|
||||
f"Das Aktualisierungs-Manifest konnte nicht geladen werden.\n\n{err}",
|
||||
parent=parent,
|
||||
)
|
||||
return
|
||||
|
||||
channel = str(data.get("channel") or "stable").strip()
|
||||
if channel != APP_CHANNEL:
|
||||
messagebox.showinfo(
|
||||
"Nach Updates suchen",
|
||||
f"Kein passendes Release fuer diesen Kanal ({APP_CHANNEL!r}).",
|
||||
parent=parent,
|
||||
)
|
||||
return
|
||||
|
||||
info = check_for_updates()
|
||||
if not info:
|
||||
messagebox.showinfo(
|
||||
"Nach Updates suchen",
|
||||
f"AZA {APP_VERSION} ist aktuell.\n\nEs ist kein neueres Release eingetragen.",
|
||||
parent=parent,
|
||||
)
|
||||
return
|
||||
|
||||
_show_update_notification(info, parent=parent)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
m, e = fetch_remote_manifest()
|
||||
print({"manifest_error": e, "keys": list(m.keys()) if m else None})
|
||||
print(check_for_updates())
|
||||
@@ -0,0 +1,48 @@
|
||||
# publish_update.ps1 – Upload Installer + Manifest nach Hetzner
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$projectRoot = $PSScriptRoot
|
||||
$remoteHost = "root@178.104.51.177"
|
||||
$remotePath = "/root/aza-app/release"
|
||||
$installerPath = Join-Path $projectRoot "dist\installer\aza_desktop_setup.exe"
|
||||
$manifestPath = Join-Path $projectRoot "release\version.json"
|
||||
|
||||
Write-Host ""
|
||||
|
||||
# --- [1/4] ---
|
||||
Write-Host "[1/4] Lokale Dateien pruefen..."
|
||||
if (-not (Test-Path $installerPath)) {
|
||||
Write-Error "ABBRUCH: $installerPath nicht gefunden."
|
||||
exit 1
|
||||
}
|
||||
$sizeMB = [math]::Round((Get-Item $installerPath).Length / 1MB, 2)
|
||||
Write-Host " Installer OK ($sizeMB MB)"
|
||||
if (-not (Test-Path $manifestPath)) {
|
||||
Write-Error "ABBRUCH: $manifestPath nicht gefunden."
|
||||
exit 1
|
||||
}
|
||||
Write-Host " version.json OK"
|
||||
|
||||
# --- [2/4] ---
|
||||
Write-Host "[2/4] Installer hochladen..."
|
||||
scp "$installerPath" "${remoteHost}:${remotePath}/aza_desktop_setup.exe"
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Error "ABBRUCH: Installer-Upload fehlgeschlagen."
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- [3/4] ---
|
||||
Write-Host "[3/4] Manifest hochladen..."
|
||||
scp "$manifestPath" "${remoteHost}:${remotePath}/version.json"
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Error "ABBRUCH: Manifest-Upload fehlgeschlagen."
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- [4/4] ---
|
||||
Write-Host "[4/4] Remote-Verifizierung..."
|
||||
ssh $remoteHost "ls -lh ${remotePath}/aza_desktop_setup.exe; ls -lh ${remotePath}/version.json; echo '---'; curl -sI https://api.aza-medwork.ch/downloads/aza_desktop_setup.exe | head -3; echo '---'; curl -sI https://api.aza-medwork.ch/download/version.json | head -3"
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "UPLOAD FERTIG"
|
||||
Write-Host ""
|
||||
@@ -0,0 +1,215 @@
|
||||
# release.ps1 – Verbindlicher Single-Entry-Point fuer den lokalen AZA Release-Build
|
||||
#
|
||||
# Reihenfolge:
|
||||
# 1. build_exe.ps1 (PyInstaller + Pre-Build-Validierung)
|
||||
# 2. build_installer.ps1 (Inno Setup Installer)
|
||||
# 3. build_release_manifest.ps1 (version.json aus aza_version.py)
|
||||
# 4. Post-Build-Verifizierung
|
||||
# 5. Abschlussmeldung mit Upload-Befehlen
|
||||
#
|
||||
# Verbindliche Release-Artefakte:
|
||||
# dist\installer\aza_desktop_setup.exe
|
||||
# release\version.json
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$projectRoot = $PSScriptRoot
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "=============================================="
|
||||
Write-Host " AZA Release-Build"
|
||||
Write-Host "=============================================="
|
||||
Write-Host ""
|
||||
|
||||
# --- Schritt 1: Desktop EXE bauen ---
|
||||
Write-Host "[1/4] Desktop EXE bauen (build_exe.ps1)..."
|
||||
Write-Host ""
|
||||
& (Join-Path $projectRoot "build_exe.ps1")
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Error "ABBRUCH: build_exe.ps1 fehlgeschlagen."
|
||||
exit 1
|
||||
}
|
||||
Write-Host ""
|
||||
|
||||
# --- Schritt 2: Installer bauen ---
|
||||
Write-Host "[2/4] Installer bauen (build_installer.ps1)..."
|
||||
Write-Host ""
|
||||
& (Join-Path $projectRoot "build_installer.ps1")
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Error "ABBRUCH: build_installer.ps1 fehlgeschlagen."
|
||||
exit 1
|
||||
}
|
||||
Write-Host ""
|
||||
|
||||
# --- Schritt 3: Release-Manifest aktualisieren ---
|
||||
Write-Host "[3/4] Release-Manifest aktualisieren (build_release_manifest.ps1)..."
|
||||
Write-Host ""
|
||||
& (Join-Path $projectRoot "build_release_manifest.ps1")
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Error "ABBRUCH: build_release_manifest.ps1 fehlgeschlagen."
|
||||
exit 1
|
||||
}
|
||||
Write-Host ""
|
||||
|
||||
# --- Schritt 4: Post-Build-Verifizierung ---
|
||||
Write-Host "[4/4] Post-Build-Verifizierung..."
|
||||
Write-Host ""
|
||||
|
||||
$internalDir = Join-Path $projectRoot "dist\aza_desktop\_internal"
|
||||
$installerPath = Join-Path $projectRoot "dist\installer\aza_desktop_setup.exe"
|
||||
$manifestPath = Join-Path $projectRoot "release\version.json"
|
||||
$errors = @()
|
||||
|
||||
# 4a) backend_url.txt pruefen
|
||||
$urlFile = Join-Path $internalDir "backend_url.txt"
|
||||
if (-not (Test-Path $urlFile)) {
|
||||
$errors += "backend_url.txt fehlt in _internal"
|
||||
} else {
|
||||
$urlContent = (Get-Content $urlFile -Raw).Trim()
|
||||
if (-not $urlContent) {
|
||||
$errors += "backend_url.txt in _internal ist leer"
|
||||
} elseif ($urlContent -match "127\.0\.0\.1|localhost|0\.0\.0\.0") {
|
||||
$errors += "backend_url.txt in _internal enthaelt lokale Adresse: $urlContent"
|
||||
} else {
|
||||
Write-Host " backend_url.txt OK: $urlContent"
|
||||
}
|
||||
}
|
||||
|
||||
# 4b) backend_token.txt pruefen
|
||||
$tokenFile = Join-Path $internalDir "backend_token.txt"
|
||||
if (-not (Test-Path $tokenFile)) {
|
||||
$errors += "backend_token.txt fehlt in _internal"
|
||||
} else {
|
||||
$tokenContent = (Get-Content $tokenFile -Raw).Trim()
|
||||
if (-not $tokenContent) {
|
||||
$errors += "backend_token.txt in _internal ist leer"
|
||||
} elseif ($tokenContent -match "CHANGE_ME") {
|
||||
$errors += "backend_token.txt in _internal enthaelt Platzhalter"
|
||||
} else {
|
||||
Write-Host " backend_token.txt OK (Token vorhanden)"
|
||||
}
|
||||
}
|
||||
|
||||
# 4c) Verbotene Dateien duerfen NICHT in _internal liegen
|
||||
$forbidden = @(".env", "license_url.txt")
|
||||
foreach ($f in $forbidden) {
|
||||
$fp = Join-Path $internalDir $f
|
||||
if (Test-Path $fp) {
|
||||
$errors += "VERBOTEN: $f liegt in _internal (darf nicht im Installer sein)"
|
||||
}
|
||||
}
|
||||
$forbiddenDb = Join-Path $internalDir "data\stripe_webhook.sqlite"
|
||||
if (Test-Path $forbiddenDb) {
|
||||
$errors += "VERBOTEN: data\stripe_webhook.sqlite liegt in _internal (darf nicht im Installer sein)"
|
||||
}
|
||||
if ($errors.Count -eq 0) {
|
||||
Write-Host " Keine verbotenen Dateien in _internal"
|
||||
}
|
||||
|
||||
# 4f) Empfang-Web-Huelle: PyInstaller-Ausgabe und Kopie neben aza_desktop muessen identisch sein
|
||||
$shellExeRoot = Join-Path $projectRoot "dist\AZA_EmpfangShell.exe"
|
||||
$shellExeDist = Join-Path $projectRoot "dist\aza_desktop\AZA_EmpfangShell.exe"
|
||||
if (-not (Test-Path $shellExeRoot)) {
|
||||
$errors += "AZA_EmpfangShell.exe fehlt in dist\ (PyInstaller-Ausgabe) - build_exe.ps1 pruefen"
|
||||
} elseif (-not (Test-Path $shellExeDist)) {
|
||||
$errors += "AZA_EmpfangShell.exe fehlt in dist\aza_desktop\ - build_exe.ps1 muss die Shell kopieren"
|
||||
} else {
|
||||
$tRoot = (Get-Item $shellExeRoot).LastWriteTime
|
||||
$tDesk = (Get-Item $shellExeDist).LastWriteTime
|
||||
$hRoot = (Get-FileHash -Path $shellExeRoot -Algorithm SHA256).Hash
|
||||
$hDesk = (Get-FileHash -Path $shellExeDist -Algorithm SHA256).Hash
|
||||
Write-Host (" AZA_EmpfangShell.exe dist\: Zeit {0:yyyy-MM-dd HH:mm:ss}, SHA256={1}" -f $tRoot, $hRoot)
|
||||
Write-Host (" AZA_EmpfangShell.exe desktop\: Zeit {0:yyyy-MM-dd HH:mm:ss}, SHA256={1}" -f $tDesk, $hDesk)
|
||||
if ($hRoot -ne $hDesk) {
|
||||
$errors += "AZA_EmpfangShell.exe: SHA256-Unterschied zwischen dist\ und dist\aza_desktop\ - Build inkonsistent"
|
||||
} else {
|
||||
Write-Host " AZA_EmpfangShell.exe: dist\ und dist\aza_desktop\ identisch OK"
|
||||
}
|
||||
}
|
||||
|
||||
# 4d) Installer existiert und hat Groesse > 0
|
||||
if (-not (Test-Path $installerPath)) {
|
||||
$errors += "Installer nicht gefunden: $installerPath"
|
||||
} else {
|
||||
$fileInfo = Get-Item $installerPath
|
||||
if ($fileInfo.Length -eq 0) {
|
||||
$errors += "Installer hat Groesse 0: $installerPath"
|
||||
}
|
||||
}
|
||||
|
||||
# 4e) version.json existiert und Version stimmt mit aza_version.py ueberein
|
||||
if (-not (Test-Path $manifestPath)) {
|
||||
$errors += "release\version.json nicht gefunden"
|
||||
} else {
|
||||
try {
|
||||
$manifest = Get-Content $manifestPath -Raw | ConvertFrom-Json
|
||||
$manifestVersion = $manifest.version
|
||||
$azaContent = Get-Content (Join-Path $projectRoot "aza_version.py") -Raw
|
||||
if ($azaContent -match 'APP_VERSION\s*=\s*"([^"]+)"') {
|
||||
$codeVersion = $matches[1].Trim()
|
||||
if ($manifestVersion -ne $codeVersion) {
|
||||
$errors += "version.json Version ($manifestVersion) stimmt nicht mit aza_version.py ($codeVersion) ueberein"
|
||||
} else {
|
||||
Write-Host " version.json OK: Version $manifestVersion"
|
||||
}
|
||||
}
|
||||
$dlUrl = $manifest.download_url
|
||||
if ($dlUrl -and ($dlUrl -match "127\.0\.0\.1|localhost")) {
|
||||
$errors += "version.json download_url enthaelt localhost: $dlUrl"
|
||||
}
|
||||
$mfSha = $manifest.sha256
|
||||
if ($mfSha -and (Test-Path $installerPath)) {
|
||||
$instSha = (Get-FileHash -Path $installerPath -Algorithm SHA256).Hash
|
||||
$mfShaNorm = $mfSha.ToString().Trim()
|
||||
if ($mfShaNorm -and ($mfShaNorm -ne $instSha)) {
|
||||
$errors += "version.json sha256 stimmt nicht mit Installer ueberein (Manifest=$mfShaNorm Installer=$instSha)"
|
||||
} elseif ($mfShaNorm) {
|
||||
Write-Host " sha256 OK: Manifest und Installer identisch"
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
$errors += "version.json konnte nicht gelesen werden: $_"
|
||||
}
|
||||
}
|
||||
|
||||
# Fehler auswerten
|
||||
if ($errors.Count -gt 0) {
|
||||
Write-Host ""
|
||||
Write-Host "=============================================="
|
||||
Write-Host " RELEASE-VERIFIZIERUNG FEHLGESCHLAGEN"
|
||||
Write-Host "=============================================="
|
||||
foreach ($e in $errors) {
|
||||
Write-Host " FEHLER: $e" -ForegroundColor Red
|
||||
}
|
||||
Write-Host ""
|
||||
Write-Error "Release-Build nicht sauber. Bitte Fehler beheben und erneut ausfuehren."
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- Abschlussmeldung ---
|
||||
$instInfo = Get-Item $installerPath
|
||||
$sizeMB = [math]::Round($instInfo.Length / 1MB, 2)
|
||||
$sha256 = (Get-FileHash -Path $installerPath -Algorithm SHA256).Hash
|
||||
|
||||
$stamp = Get-Date -Format "yyyyMMdd_HHmmss"
|
||||
$stampedInstaller = Join-Path $projectRoot "dist\installer\aza_desktop_setup_$stamp.exe"
|
||||
try {
|
||||
Copy-Item -LiteralPath $installerPath -Destination $stampedInstaller -Force
|
||||
Write-Host " Zeitstempel-Kopie des Installers: $stampedInstaller"
|
||||
} catch {
|
||||
Write-Warning "Zeitstempel-Kopie konnte nicht erstellt werden: $_"
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "=============================================="
|
||||
Write-Host " RELEASE BEREIT FUER UPLOAD"
|
||||
Write-Host "=============================================="
|
||||
Write-Host ""
|
||||
Write-Host " Installer: $installerPath"
|
||||
Write-Host " Groesse: $sizeMB MB"
|
||||
Write-Host " SHA256: $sha256"
|
||||
Write-Host " Manifest: $manifestPath"
|
||||
Write-Host ""
|
||||
Write-Host " Upload-Befehle (Produktion laut Vorgabe; ggf. andere IP in Staging-Umgebungen):"
|
||||
Write-Host " scp `"$installerPath`" root@178.104.51.177:/root/aza-app/release/aza_desktop_setup.exe"
|
||||
Write-Host " scp `"$manifestPath`" root@178.104.51.177:/root/aza-app/release/version.json"
|
||||
Write-Host ""
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"version": "1.2.0",
|
||||
"build": "20260314_120000",
|
||||
"channel": "stable",
|
||||
"release_date": "2026-03-14",
|
||||
"minimum_supported_version": "1.0.0",
|
||||
"min_required_version": "1.0.0",
|
||||
"download_url": "https://api.aza-medwork.ch/downloads/aza_desktop_setup.exe",
|
||||
"update_level": "recommended",
|
||||
"installer_type": "inno-setup",
|
||||
"notes": [
|
||||
"Erste stabile Verkaufsversion",
|
||||
"6 Module: KI-Assistent, Krankengeschichte, Audio-Notizen, Uebersetzer, Aerzte-Netzwerk, Praxis-Intern",
|
||||
"Automatischer lokaler Backend-Start",
|
||||
"Integrierter Systemstatus mit 12 Pruefpunkten",
|
||||
"Professioneller Launcher mit Modulauswahl",
|
||||
"Projekt-Notizen mit Bild-Einfuegen und Diktat",
|
||||
"OpenAI-Key Setup-Helfer",
|
||||
"Windows-Firewall wird automatisch konfiguriert"
|
||||
],
|
||||
"release_notes": [
|
||||
"Erste stabile Verkaufsversion",
|
||||
"6 Module: KI-Assistent, Krankengeschichte, Audio-Notizen, Uebersetzer, Aerzte-Netzwerk, Praxis-Intern",
|
||||
"Automatischer lokaler Backend-Start",
|
||||
"Integrierter Systemstatus mit 12 Pruefpunkten",
|
||||
"Professioneller Launcher mit Modulauswahl",
|
||||
"Projekt-Notizen mit Bild-Einfuegen und Diktat",
|
||||
"OpenAI-Key Setup-Helfer",
|
||||
"Windows-Firewall wird automatisch konfiguriert"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user