# Android-Beta-Downloadbereich, Update-Check und Release-Ablauf (v1.32.0) ## Build 55 / v1.52.4 Geraete-Artefaktstatus (2026-08-13) Ziel fuer diesen Android-Patch ist ein lokal mit dem kanonischen BENSA-Geraete-Signer signierter Abnahmekandidat: - Version: `1.52.4` - Android `versionCode`: `55` - Vorheriges Geraete-Artefakt: `bensa-wetter-v1.52.3-1cd00e3-build54-local-debug.apk` - Vorherige SHA-256: `2da4a31e57a0fdceda0867a3374fea7dc0e9b5c3d1b66bebcf7edd377f41ecf8` - Verbindlicher Signer: `0A7E9201B167334E2EA3EEBE238EFCDBF23B091FAC50EDE8FBB27D57B45F0261` **Ausgefuehrt fuer Build 55:** `bensa-wetter-v1.52.4-9a587e2-build55-local-debug.apk` wurde lokal gebaut und verifiziert. SHA-256: `722fa8bff2369636ee861382d43059512a1e660f6c15b4b83d03f94154baee80`. `verify-release-artifact` inklusive `--previous-apk bensa-wetter-v1.52.3-1cd00e3-build54-local-debug.apk --verify-upgrade` meldete PASS: Package `eu.comhair.bensa.mobile`, versionCode 55, versionName 1.52.4, eingebetteter Commit `9a587e2`, Signer `0A7E9201B167334E2EA3EEBE238EFCDBF23B091FAC50EDE8FBB27D57B45F0261`, Datenmarker `release-gate-1786647348398` nach `adb install -r` erhalten. Es wurde kein `publish-android-release.js` ausgefuehrt; keine automatische Beta-/Public-Verteilung. ## Build 54 / v1.52.3 Geraete-Artefaktstatus (2026-08-13) Ziel fuer diesen Android-Patch ist ein neuer, lokal mit dem kanonischen BENSA-Geraete-Signer signierter Abnahmekandidat: - Version: `1.52.3` - Android `versionCode`: `54` - Vorheriges Geraete-Artefakt: `bensa-wetter-v1.52.2-f3f3214-local-debug.apk` - Vorherige SHA-256: `540f37201a53a1abd4f4de1dde243d162276574830e5b1532d9c0869a74af84f` - Verbindlicher Signer: `0A7E9201B167334E2EA3EEBE238EFCDBF23B091FAC50EDE8FBB27D57B45F0261` Das neue Artefakt darf erst freigegeben werden, wenn `verify-release-artifact` mit `--previous-apk bensa-wetter-v1.52.2-f3f3214-local-debug.apk --verify-upgrade` PASS meldet, Package-ID und Signer identisch sind, `versionCode` steigt und der Datenmarker beim echten `adb install -r` erhalten bleibt. CI-Debug-APKs bleiben test-only und duerfen nicht als Pixel-/ Geraete-Abnahmekandidaten verwendet werden. **Ausgefuehrt fuer Build 54:** `bensa-wetter-v1.52.3-1cd00e3-build54-local-debug.apk` wurde lokal gebaut und verifiziert. SHA-256: `2da4a31e57a0fdceda0867a3374fea7dc0e9b5c3d1b66bebcf7edd377f41ecf8`. `verify-release-artifact` inklusive `--previous-apk bensa-wetter-v1.52.2-f3f3214-local-debug.apk --verify-upgrade` meldete PASS: Package `eu.comhair.bensa.mobile`, versionCode 54, versionName 1.52.3, eingebetteter Commit `1cd00e3`, Signer `0A7E9201B167334E2EA3EEBE238EFCDBF23B091FAC50EDE8FBB27D57B45F0261`, Datenmarker `release-gate-1786643468634` nach `adb install -r` erhalten. ## Zweck Beschreibt, wie eine freigegebene Android-Testversion öffentlich über die BENSA-Wetter-Webseite/ PWA bereitgestellt wird (`/downloads/android/`), wie die Android-App selbst nach Updates sucht, und den manuell vom Betreiber ausgeführten Release-Ablauf (dieses Dokument dient zugleich als Release-Handbuch für Android-Veröffentlichungen). Siehe auch `docs/android-app-architecture.md` (App-Architektur), `docs/android-build-and-test.md` (Build/Test), `docs/CURRENT_STATUS.md` (aktueller Gesamtstand). ## Geltungsbereich und Grenzen - Es gibt **keinen Google-Play-Release**. Die APK wird ausschließlich über diesen eigenen Downloadbereich verteilt ("Sideloading") - Android zeigt dabei einmalig einen Bestätigungsdialog ("Apps aus dieser Quelle zulassen"), das ist normal. - Dieser Block **entscheidet keine Signierungsstrategie** und **erzeugt keinen Signierschlüssel**. Siehe Abschnitt "Signierungsstatus" unten - das ist eine bewusst offene, dem Betreiber vorbehaltene Entscheidung. - Es wird **keine öffentliche Beta-APK in diesem Block ausgegeben**. Die Architektur (Server-Route, Manifestformat, Publish-Skript, App-Update-Check) ist vollständig implementiert und getestet, `downloads/android/` bleibt aber bis zur Signierungsentscheidung leer (siehe `downloads/android/README.md`). - CI-Debug-APKs aus GitHub Actions sind **keine** Geräte- oder Release-Artefakte. Sie dienen nur als technische Build-Nachweise und sind wegen temporärer Runner-Debug-Keys nicht updatekompatibel. ## Architektur ```text downloads/ android/ bensa-wetter-.apk # versionierte Kopie, langfristig cachebar bensa-wetter-latest.apk # stets die aktuellste veröffentlichte Datei latest.json # Versionsmanifest, siehe unten ``` Ausgeliefert von `scripts/serve-monitoring.js` unter `GET /downloads/android/*` als reine statische Dateien (`serveDownloadsStatic`, `DOWNLOADS_ROOT`) - kein API-Handler, kein Verzeichnislisting. `latest.json` wird `no-cache` ausgeliefert (ändert sich pro Release ohne URL-Änderung), versionierte `.apk`-Dateien dürfen länger gecacht werden (Version steckt im Dateinamen), `bensa-wetter-latest.apk` bleibt kurz gecacht (Inhalt ändert sich unter derselben URL). Diese drei Dateiarten sind **nicht Teil des Git-Repositorys** (siehe `.gitignore`) - es sind Release-Artefakte, keine Quelltexte, die der Betreiber zusätzlich auf den Produktionsserver übertragen muss (siehe "Veröffentlichung" unten). ## Versionsmanifest (`latest.json`) Schema/Validierung: `scripts/lib/android-release-manifest-lib.js` (`validateManifest`, `isVersionCodeIncreasing`). Pflichtfelder: | Feld | Bedeutung | |---|---| | `platform` | immer `"android"` | | `channel` | z. B. `"beta"` | | `versionName` | z. B. `"1.32.0"` | | `versionCode` | muss bei jedem Release steigen (hart erzwungen, siehe unten) | | `productVersion` | zentrale Produktversion (`scripts/lib/version-info.js`) | | `gitCommit` | kurzer Commit-Hash des veröffentlichten Stands | | `publishedAt` | ISO-Zeitstempel | | `minSdk` | Mindest-Android-API-Level | | `downloadUrl` | vollständige HTTPS-URL zur versionierten APK | | `fileSizeBytes` | Dateigröße | | `sha256` | Prüfsumme (64-stelliger Hex-String, Kleinbuchstaben) | | `mandatory` | reines Anzeige-Flag ("dringend empfohlen") - **erzwingt niemals** eine Installation | | `releaseNotes` | Array der wichtigsten Änderungen | ## Veröffentlichung Manuell, lokal, vom Betreiber ausgeführt - **kein automatischer CI-Schritt**: ```bash node scripts/publish-android-release.js \ --apk /pfad/zur/signierten-beta.apk \ --versionName 1.32.0 \ --versionCode 4 \ --minSdk 26 \ --channel beta \ --notes /pfad/zu/release-notes.txt ``` Das Skript signiert nichts selbst, bricht bei einem nicht steigenden `versionCode` hart ab (kein Duplikat/Rücksprung möglich), validiert das Manifest vor dem Schreiben und gibt anschließend einen Hinweis aus, dass die erzeugten Dateien zusätzlich auf den Produktionsserver übertragen werden müssen (z. B. per `scp`/`rsync` in das dortige `downloads/android/`-Verzeichnis, außerhalb dieses Git-Repositorys). ## Artefaktpolitik für Geräte-Abnahme Ein installierbarer BENSA-Geräte-/Pixel-Abnahmekandidat ist ausschließlich ein eindeutig benanntes APK mit folgenden dokumentierten Eigenschaften: - Produktversion (`versionName`) - Android-Buildnummer (`versionCode`) - Commit des eingebetteten `BuildConfig.GIT_COMMIT` - SHA-256 der APK-Datei - Signer-Fingerprint SHA-256 - Rolle des Artefakts, z. B. `device-acceptance` Generische Namen wie `app-debug.apk` sind nur interne Gradle-Ausgabepfade. Sie dürfen nicht als freigegebenes Abnahmeartefakt verteilt werden. Das aktuelle Build-53-Geräteartefakt lautet `bensa-wetter-v1.52.2-f3f3214-local-debug.apk`, SHA-256 `540f37201a53a1abd4f4de1dde243d162276574830e5b1532d9c0869a74af84f`, Signer `0A7E9201B167334E2EA3EEBE238EFCDBF23B091FAC50EDE8FBB27D57B45F0261`. Vor jeder Gerätefreigabe muss das Release-Gate gegen die zuletzt freigegebene Geräte-APK laufen: ```bash node scripts/verify-release-artifact.js \ --apk \ --manifest docs/release-manifest-v.json \ --previous-apk \ --verify-upgrade ``` Dieses Gate prüft zusätzlich zum normalen Artefaktabgleich die Upgrade-Kompatibilität (identische Package-ID, monoton steigender `versionCode`, identischer Signer) und führt bei `--verify-upgrade` den echten `adb install -r`-Test inklusive Datenmarker aus. Ein Clean-Install ersetzt diesen Nachweis nie. ## Signierungsstatus (verbindlich vor jeder ersten öffentlichen Beta zu berichten) - **Aktuell verwendete Signierung:** ausschließlich der Android-Debug-Keystore (`~/.android/debug.keystore`, automatisch von den Android-SDK-Build-Tools erzeugt). Es existiert **kein** `signingConfigs`-Block in `mobile/android/app/build.gradle.kts` und **kein** eigener Release-Keystore in diesem Repository oder auf dem Entwicklungsrechner. - **Kann die installierte Debug-App mit einer späteren Beta aktualisiert werden?** **Nein.** Der Debug-Keystore ist auf jedem Entwicklungsrechner individuell (bei jeder Neuerstellung potenziell neu) - eine mit einem *anderen* Schlüssel signierte APK gilt Android gegenüber einer bereits installierten Debug-APK als "andere App" mit gleicher `applicationId`, obwohl sie es technisch nicht ist. - **Wird eine einmalige Deinstallation nötig sein?** **Ja**, beim Wechsel von der aktuellen Debug-signierten Testinstallation auf die erste mit einem dedizierten Beta-Schlüssel signierte Version. Danach (Beta-zu-Beta) sind Updates über eine vorhandene Installation möglich, solange derselbe Beta-Schlüssel weiterverwendet wird. - **Wie soll der künftige Schlüssel sicher gespeichert/gesichert werden?** Vorschlag (noch nicht umgesetzt, Betreiberentscheidung ausstehend): ein dedizierter Release-Keystore außerhalb des Git-Repositorys (siehe `.gitignore`-Einträge `*.jks`/`*.keystore`), Keystore-Passwort und Key-Passwort ausschließlich als lokale Umgebungsvariablen/`gradle.properties` außerhalb des Repositorys, plus ein verschlüsseltes Offline-Backup (z. B. Passwort-Manager-Datei), da ein verlorener Schlüssel bedeuten würde, dass künftige Updates nicht mehr über bestehende Installationen ausgeliefert werden könnten. - **Entscheidung offen:** Diese Fragen sind bewusst nur beantwortet, nicht entschieden - es wird **kein** Schlüssel erzeugt und **keine** Strategie produktiv festgelegt, ohne dass der Betreiber dies ausdrücklich freigibt. ## Signierungs-Entscheidungsvorlage (Betreiberentscheidung, kein Schlüssel erzeugt) Drei Optionen, damit der Betreiber zwischen ihnen entscheiden kann. Keine der drei ist bereits umgesetzt oder vorbereitet über das hier Beschriebene hinaus. **Option A - Lokale Signierung** Der Release-Keystore liegt ausschließlich auf einem einzelnen vom Betreiber kontrollierten Rechner, die signierte APK wird lokal gebaut (`./gradlew assembleRelease` mit lokal gesetztem `signingConfigs`-Block) und danach manuell über `scripts/publish-android-release.js` veröffentlicht (dieses Skript existiert bereits und signiert selbst nichts, siehe oben). - Vorteil: Schlüssel verlässt nie einen einzigen Rechner, kein CI-Secret-Handling nötig. - Nachteil: Release ist an diesen einen Rechner gebunden, kein Wiederherstellungsweg außer Backup, keine Automatisierung möglich. **Option B - CI-Signierung** Der Release-Keystore liegt verschlüsselt als GitHub-Actions-Secret, `android-build.yml` (oder ein neuer, getrennter Release-Workflow) baut und signiert die APK automatisch bei einem manuell ausgelösten Release-Trigger (kein automatischer Trigger bei jedem Push - Signierung bleibt ein bewusster, manuell angestoßener Schritt). - Vorteil: reproduzierbarer, dokumentierter Build-Prozess, kein lokaler Rechner als Single Point of Failure. - Nachteil: Schlüssel/Passwort müssen als GitHub-Secret hinterlegt werden (zusätzliche Angriffsfläche, erfordert sorgfältige Zugriffsbeschränkung auf das Repository). **Option C - Hybrider Prozess** Schlüsselerzeugung und -verwahrung lokal (wie Option A), aber der eigentliche Build/Signiervorgang optional auch über CI mit demselben, aus einem lokalen, verschlüsselten Backup importierten Secret - lokale Signierung bleibt der Normalfall, CI ist nur ein Ausweichweg (z. B. wenn der primäre Rechner nicht verfügbar ist). - Vorteil: kombiniert Kontrolle (Option A) mit Ausfallsicherheit (Option B). - Nachteil: höchste Komplexität, zwei Prozesse müssen synchron gehalten werden (Schlüsselversion, Passwort-Rotation). **Vergleich entlang der vier entscheidungsrelevanten Dimensionen:** | Dimension | Option A (lokal) | Option B (CI-Secret) | Option C (hybrid) | |---|---|---|---| | **Sicherheit** | Höchste — Schlüssel verlässt nie einen Rechner, keine zusätzliche Angriffsfläche über das eigene GitHub-Konto/Repository hinaus. | Niedriger — ein kompromittiertes GitHub-Konto oder Repository-Zugriff (z. B. gestohlenes Token, falsch gesetzte Berechtigung) könnte den Schlüssel offenlegen; Absicherung braucht Branch-Schutz, eingeschränkte Secret-Sichtbarkeit und 2FA auf dem GitHub-Konto. | Mittel — dieselbe Angriffsfläche wie B, aber nur als Ausweichweg genutzt (kleineres Zeitfenster, in dem das Secret überhaupt in CI vorliegt). | | **Aufwand (laufend)** | Gering pro Release, aber jeder Release erfordert den physischen/lokalen Rechner mit installiertem JDK/Android-SDK und Zugriff auf den Keystore. | Höher initial (Workflow/Secret-Einrichtung), danach am geringsten pro Release (ein Trigger-Klick). | Am höchsten — zwei Prozesse (lokal und CI) müssen synchron gehalten werden (Schlüsselversion, Passwort-Rotation), doppelter Pflegeaufwand. | | **Wiederherstellung bei Schlüsselverlust** | Nur so gut wie das eigene Backup (siehe „Backupstrategie" unten) — kein zweiter Ort, an dem der Schlüssel je existiert hat. | Ebenfalls nur so gut wie das Backup, aber das GitHub-Secret selbst ist kein Backup (Secrets sind bei GitHub nicht im Klartext abrufbar) — bei Secret-Verlust muss ohnehin auf das lokale Backup zurückgegriffen werden. | Bestes Wiederherstellungsprofil — CI kann bei nicht verfügbarem Primärrechner einspringen, sofern das Secret aktuell gehalten wird; setzt aber diszipliniertes Nachziehen bei jeder Passwort-Rotation voraus. | | **Updatekontinuität für bestehende Installationen** | Unverändert gut, solange derselbe Keystore/Rechner (oder ein wiederhergestelltes Backup davon) verwendet wird. | Unverändert gut, solange dasselbe Secret im Repository bleibt. | Unverändert gut, solange lokaler Keystore und CI-Secret **immer denselben** Schlüssel enthalten — die größte praktische Fehlerquelle dieser Option ist ein unbemerktes Auseinanderlaufen der beiden Kopien. | **Bereits vorhanden, unabhängig von der Signierungsoption:** `versionCode`/`versionName` (`mobile/android/app/build.gradle.kts`), Versionsmanifest-Validierung (`scripts/lib/android-release-manifest-lib.js` - SHA-256, ansteigender `versionCode`, siehe `tests/run-android-release-manifest-tests.js`), Downloadstruktur (`downloads/android/`, siehe Abschnitt "Architektur" oben). **Rollback:** noch nicht vorbereitet - aktuell existiert nur `latest.json` (immer die neueste Version), kein Mechanismus, um eine veröffentlichte Version gezielt zurückzurufen oder eine ältere Version erneut als "aktuell" zu markieren; das müsste bei Bedarf ergänzt werden (z. B. ein `rollback`-Feld oder Beibehaltung mehrerer Manifest-Versionen). **Klare Empfehlung für BENSA Wetter (nicht bindend, Betreiberentscheidung steht weiterhin aus): Option A.** Begründung anhand der Vergleichstabelle oben: BENSA Wetter ist ein Ein-Personen- Betreiberprojekt mit unregelmäßigen, manuell ausgelösten Releases (kein Team, kein hoher Release-Takt) — genau das Profil, für das Option A am besten passt: höchste Sicherheit (keine zusätzliche Angriffsfläche über das GitHub-Konto hinaus), geringster laufender Aufwand pro Release für eine einzelne Person, und die Wiederherstellung hängt ohnehin bei allen drei Optionen von derselben Backupstrategie ab (siehe unten) - Option C erkauft sich dafür ausschließlich mehr Komplexität (zwei synchron zu haltende Kopien) ohne einen Sicherheits- oder Wiederherstellungsvorteil, der bei einem einzelnen Betreiber tatsächlich zum Tragen käme. Option C wird deshalb hier **nicht** trotz einer generischen "für die erste Beta besser" Faustregel empfohlen, sondern bewusst zurückgestellt, bis ein zweiter, tatsächlich unabhängig arbeitender Mitwirkender die Automatisierung rechtfertigt. Ein Wechsel von A zu B/C ist jederzeit möglich, ohne bestehende Installationen zu beeinträchtigen (derselbe Schlüssel wird nur an einen anderen Ort verschoben/zusätzlich hinterlegt). **Betreiberstopp**: diese Entscheidung wird bewusst NICHT von dieser Session getroffen - sobald der Betreiber eine der drei Optionen wählt (oder eine eigene vierte), kann der nächste Schritt (Keystore-Erzeugung, `signingConfigs`, ggf. CI-Secret) darauf aufbauen. ## Benötigte Backupstrategie (unabhängig von der gewählten Option, sobald ein Schlüssel existiert) Ein verlorener Release-Keystore bedeutet **dauerhaften Verlust der Update-Kontinuität**: jede künftige Version müsste mit einem neuen Schlüssel signiert werden, und jeder bestehenden Nutzerin/jedem Nutzer bliebe nur eine manuelle Deinstallation der alten und Neuinstallation der neuen APK. Das gilt unabhängig davon, welche der drei Optionen oben gewählt wird — deshalb ist die Backupstrategie kein Bestandteil der Wahl, sondern eine eigene, verbindliche Voraussetzung, bevor der erste Beta-Schlüssel erzeugt wird: 1. **Mindestens zwei physisch getrennte Kopien** des Keystore-Datei (`.jks`/`.keystore`) — z. B. ein verschlüsselter USB-Stick an einem anderen Ort als der Hauptrechner, plus eine verschlüsselte Cloud-Kopie (z. B. in einem Passwort-Manager mit Datei-Anhang-Funktion, nicht als Klartext-Datei in einem allgemeinen Cloud-Speicher). 2. **Keystore-Passwort und Key-Passwort getrennt vom Keystore selbst aufbewahren** (z. B. im Passwort-Manager als eigener Eintrag) — eine gefundene/gestohlene Keystore-Datei allein darf ohne die Passwörter nicht nutzbar sein. 3. **Nie im Git-Repository** — `*.jks`/`*.keystore` sind bereits in `.gitignore` ausgeschlossen; vor jedem `git add`/`git status`-Review nach einem versehentlich gestagten Keystore prüfen (siehe allgemeine Secret-Scan-Routine dieses Projekts). 4. **Nach jeder Erzeugung/Rotation des Schlüssels**: Backup-Kopien sofort aktualisieren, alte Kopien nicht kommentarlos überschreiben, sondern versioniert ablegen (z. B. Dateiname mit Erzeugungsdatum), damit ein fehlgeschlagener Rotationsversuch nicht die einzige funktionierende Kopie zerstört. 5. **Jährlicher Wiederherstellungstest** (empfohlen, nicht nur einmalig beim Erzeugen): aus der Backup-Kopie tatsächlich einmal eine Test-APK signieren und prüfen, dass sie sich über eine vorhandene Installation aktualisieren lässt — ein Backup, das nie ausprobiert wurde, ist kein verlässliches Backup. 6. **Bei Option B/C zusätzlich**: das GitHub-Secret regelmäßig gegen die lokale Backup-Kopie prüfen (derselbe SHA-256 der Keystore-Datei) - unbemerktes Auseinanderlaufen ist laut Tabelle oben die größte praktische Fehlerquelle der hybriden Option. ## Android-Update-Check (App-Seite) Siehe `mobile/android/app/src/main/kotlin/eu/comhair/bensa/mobile/data/UpdateCheckRepository.kt`, `UpdateCheckParser.kt`, `UpdateCheckModels.kt`, `UpdateCheckState.kt`. - Fragt `BuildConfig.UPDATE_MANIFEST_URL` (`/downloads/android/latest.json`) ab - ein einfacher `GET` ohne Query-Parameter, ohne Geräte-ID, ohne Tracking. - Automatischer Check beim Start ist **nicht blockierend** (läuft parallel zum Wetter-Dashboard-Abruf) und erfolgt höchstens alle 24 Stunden (`UpdateCheckRepository.shouldAutoCheck`). - Manueller Check jederzeit über **Mehr → „Nach Updates suchen"** (ignoriert das 24h-Intervall). - Vergleich ausschließlich über `versionCode` (`UpdateCheckParser.parse`) - ein fehlendes/ unplausibles Feld wird konservativ als "kein Update" behandelt, nie als Einladung fehlinterpretiert. - Bei verfügbarem Update: verständlicher Hinweistext, Dateigröße, Änderungen, Button „Update herunterladen" - öffnet **ausschließlich den Browser** (`PwaLinks.openExternal`). Es gibt **keine stille Installation**; der normale Android-Installationsdialog bleibt in jedem Fall erforderlich. ## Installationsanleitung (öffentlich, siehe auch PWA-Abschnitt „Apps & Geräte") 1. Auf der BENSA-Wetter-Webseite/PWA im Bereich „Apps & Geräte" auf „Android-App herunterladen" tippen. 2. Die Download-Benachrichtigung öffnen. 3. Falls Android einmalig fragt: „Apps aus dieser Quelle zulassen" bestätigen (normal, da die Testversion bewusst nicht über den Play Store verteilt wird). 4. Installation im erscheinenden Systemdialog bestätigen. ## Aktualisierungsanleitung - Bevorzugt: in der App unter „Mehr → Nach Updates suchen" prüfen, bei verfügbarem Update auf „Update herunterladen" tippen und die neue APK wie eine Neuinstallation über den Android-Systemdialog installieren. - Alternativ: erneut über den Downloadbereich der Webseite/PWA herunterladen. - Eine Aktualisierung über eine bestehende Installation funktioniert nur, solange `applicationId` (`eu.comhair.bensa.mobile`) und Signaturschlüssel gegenüber der installierten Version unverändert sind (siehe „Signierungsstatus" oben für den aktuellen, noch nicht produktiv entschiedenen Stand).