🔐 Add keystore management scripts and documentation [skip ci]
- Add create-keystore.fish: Generate new release keystore with auto-generated passwords - Add verify-secrets.fish: Verify GitHub Secrets and local keystore setup - Add build-release-local.fish: Build signed release APKs locally - Add LOCAL_BUILDS.md: Documentation for local release builds - Add key.properties.example: Template for signing configuration - Update android/.gitignore: Protect sensitive keystore files - Integrate GitHub CLI for automatic secret management - All scripts support both manual and automated workflows
This commit is contained in:
5
android/.gitignore
vendored
5
android/.gitignore
vendored
@@ -13,3 +13,8 @@
|
|||||||
.externalNativeBuild
|
.externalNativeBuild
|
||||||
.cxx
|
.cxx
|
||||||
local.properties
|
local.properties
|
||||||
|
|
||||||
|
# Signing configuration (contains sensitive keys)
|
||||||
|
key.properties
|
||||||
|
*.jks
|
||||||
|
*.keystore
|
||||||
|
|||||||
157
android/LOCAL_BUILDS.md
Normal file
157
android/LOCAL_BUILDS.md
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
# Lokale Gradle Builds mit Release-Signierung
|
||||||
|
|
||||||
|
Dieses Dokument erklärt, wie du lokal signierte APKs erstellst, die mit den GitHub Release-APKs kompatibel sind.
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
|
||||||
|
- **GitHub Actions** erstellt signierte Release-APKs mit dem Production-Keystore
|
||||||
|
- **Lokale Debug-Builds** verwenden einen temporären Debug-Key
|
||||||
|
- ❌ **Resultat:** Nutzer können lokale Debug-APKs NICHT über Release-APKs installieren (Signature Mismatch!)
|
||||||
|
|
||||||
|
## Lösung: Lokale Release-Builds mit Production-Key
|
||||||
|
|
||||||
|
### 1️⃣ Keystore-Konfiguration einrichten
|
||||||
|
|
||||||
|
Du hast bereits den Keystore: `/android/app/simple-notes-release.jks`
|
||||||
|
|
||||||
|
Erstelle eine `key.properties` Datei im `/android/` Ordner:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /home/liq/gitProjects/simple-notes-sync/android
|
||||||
|
cp key.properties.example key.properties
|
||||||
|
```
|
||||||
|
|
||||||
|
Bearbeite `key.properties` mit den echten Werten:
|
||||||
|
|
||||||
|
```properties
|
||||||
|
storeFile=simple-notes-release.jks
|
||||||
|
storePassword=<dein-keystore-password>
|
||||||
|
keyAlias=<dein-key-alias>
|
||||||
|
keyPassword=<dein-key-password>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Wichtig:** Die Werte müssen **exakt** mit den GitHub Secrets übereinstimmen:
|
||||||
|
- `KEYSTORE_PASSWORD` → `storePassword`
|
||||||
|
- `KEY_ALIAS` → `keyAlias`
|
||||||
|
- `KEY_PASSWORD` → `keyPassword`
|
||||||
|
|
||||||
|
### 2️⃣ Lokal signierte Release-APKs bauen
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd android
|
||||||
|
./gradlew assembleStandardRelease
|
||||||
|
```
|
||||||
|
|
||||||
|
Die signierten APKs findest du dann hier:
|
||||||
|
```
|
||||||
|
android/app/build/outputs/apk/standard/release/
|
||||||
|
├── app-standard-universal-release.apk
|
||||||
|
├── app-standard-arm64-v8a-release.apk
|
||||||
|
└── app-standard-armeabi-v7a-release.apk
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3️⃣ F-Droid Flavor bauen (optional)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gradlew assembleFdroidRelease
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4️⃣ Beide Flavors gleichzeitig bauen
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gradlew assembleStandardRelease assembleFdroidRelease
|
||||||
|
```
|
||||||
|
|
||||||
|
## Verifizierung der Signatur
|
||||||
|
|
||||||
|
Um zu prüfen, ob dein lokaler Build die gleiche Signatur wie die Release-Builds hat:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Signatur von lokalem Build anzeigen
|
||||||
|
keytool -printcert -jarfile app/build/outputs/apk/standard/release/app-standard-universal-release.apk
|
||||||
|
|
||||||
|
# Signatur von GitHub Release-APK anzeigen (zum Vergleich)
|
||||||
|
keytool -printcert -jarfile ~/Downloads/simple-notes-sync-v1.1.0-standard-universal.apk
|
||||||
|
```
|
||||||
|
|
||||||
|
Die **SHA256** Fingerprints müssen **identisch** sein!
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### ❌ Build schlägt fehl: "Keystore not found"
|
||||||
|
|
||||||
|
**Problem:** `key.properties` oder Keystore-Datei fehlt
|
||||||
|
|
||||||
|
**Lösung:**
|
||||||
|
1. Prüfe, ob `key.properties` existiert: `ls -la key.properties`
|
||||||
|
2. Prüfe, ob der Keystore existiert: `ls -la app/simple-notes-release.jks`
|
||||||
|
|
||||||
|
### ❌ "Signature mismatch" beim Update
|
||||||
|
|
||||||
|
**Problem:** Der lokale Build verwendet einen anderen Key als die Release-Builds
|
||||||
|
|
||||||
|
**Lösung:**
|
||||||
|
1. Vergleiche die Signaturen mit `keytool` (siehe oben)
|
||||||
|
2. Stelle sicher, dass `key.properties` die **exakten** GitHub Secret-Werte enthält
|
||||||
|
3. Deinstalliere die alte Version und installiere die neue (als letzter Ausweg)
|
||||||
|
|
||||||
|
### ❌ Build verwendet Debug-Signatur
|
||||||
|
|
||||||
|
**Problem:** `build.gradle.kts` findet `key.properties` nicht
|
||||||
|
|
||||||
|
**Lösung:**
|
||||||
|
```bash
|
||||||
|
# Prüfe, ob die Datei im richtigen Verzeichnis liegt
|
||||||
|
ls -la android/key.properties # ✅ Richtig
|
||||||
|
ls -la android/app/key.properties # ❌ Falsch
|
||||||
|
```
|
||||||
|
|
||||||
|
## Sicherheitshinweise
|
||||||
|
|
||||||
|
⚠️ **NIEMALS** diese Dateien committen:
|
||||||
|
- `key.properties` (in `.gitignore`)
|
||||||
|
- `*.jks` / `*.keystore` (in `.gitignore`)
|
||||||
|
|
||||||
|
✅ **Schon in `.gitignore`:**
|
||||||
|
```gitignore
|
||||||
|
key.properties
|
||||||
|
*.jks
|
||||||
|
*.keystore
|
||||||
|
```
|
||||||
|
|
||||||
|
⚠️ Die GitHub Secrets (`KEYSTORE_PASSWORD`, etc.) und die lokale `key.properties` müssen **synchron** bleiben!
|
||||||
|
|
||||||
|
## Workflow-Vergleich
|
||||||
|
|
||||||
|
### GitHub Actions Build
|
||||||
|
```yaml
|
||||||
|
- Lädt Keystore aus Base64 Secret
|
||||||
|
- Erstellt key.properties aus Secrets
|
||||||
|
- Baut mit: ./gradlew assembleStandardRelease
|
||||||
|
- ✅ Produktions-signiert
|
||||||
|
```
|
||||||
|
|
||||||
|
### Lokaler Build
|
||||||
|
```bash
|
||||||
|
# Mit key.properties konfiguriert:
|
||||||
|
./gradlew assembleStandardRelease
|
||||||
|
# ✅ Produktions-signiert (gleiche Signatur wie GitHub!)
|
||||||
|
|
||||||
|
# Ohne key.properties:
|
||||||
|
./gradlew assembleStandardRelease
|
||||||
|
# ⚠️ Debug-signiert (inkompatibel mit Releases!)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quick Reference
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Release-APK bauen (signiert, klein, optimiert)
|
||||||
|
./gradlew assembleStandardRelease
|
||||||
|
|
||||||
|
# Debug-APK bauen (unsigniert, groß, debuggable)
|
||||||
|
./gradlew assembleStandardDebug
|
||||||
|
|
||||||
|
# APK per HTTP Server verteilen
|
||||||
|
cd app/build/outputs/apk/standard/release
|
||||||
|
python3 -m http.server 8892
|
||||||
|
```
|
||||||
21
android/key.properties.example
Normal file
21
android/key.properties.example
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# Android Signing Configuration
|
||||||
|
#
|
||||||
|
# ANLEITUNG FÜR LOKALE BUILDS:
|
||||||
|
# 1. Kopiere diese Datei nach "key.properties" (ohne .example)
|
||||||
|
# 2. Fülle die Werte mit deinen echten Keystore-Daten aus
|
||||||
|
# 3. Die key.properties Datei ist in .gitignore und wird NICHT committet
|
||||||
|
#
|
||||||
|
# WICHTIG: Diese Datei darf NIEMALS ins Git-Repository gelangen!
|
||||||
|
# Sie enthält sensible Signing-Keys für die App-Veröffentlichung.
|
||||||
|
|
||||||
|
# Pfad zum Keystore (relativ zum android/app Ordner)
|
||||||
|
storeFile=simple-notes-release.jks
|
||||||
|
|
||||||
|
# Keystore Password
|
||||||
|
storePassword=DEIN_KEYSTORE_PASSWORD
|
||||||
|
|
||||||
|
# Key Alias (meist "key0" oder ein selbst gewählter Name)
|
||||||
|
keyAlias=DEIN_KEY_ALIAS
|
||||||
|
|
||||||
|
# Key Password
|
||||||
|
keyPassword=DEIN_KEY_PASSWORD
|
||||||
192
android/scripts/README.md
Normal file
192
android/scripts/README.md
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
# Android Build Scripts
|
||||||
|
|
||||||
|
Nützliche Scripts für die lokale Entwicklung und Release-Erstellung.
|
||||||
|
|
||||||
|
## 📜 Verfügbare Scripts
|
||||||
|
|
||||||
|
### 1. `create-keystore.fish` - Neuen Release-Keystore erstellen
|
||||||
|
|
||||||
|
**Wann verwenden:**
|
||||||
|
- ✅ Erstmaliges Setup des Projekts
|
||||||
|
- ✅ Keystore-Passwort vergessen
|
||||||
|
- ✅ Keystore beschädigt oder verloren
|
||||||
|
- ❌ **NICHT** verwenden, wenn bereits User existieren (macht alte APKs inkompatibel!)
|
||||||
|
|
||||||
|
**Verwendung:**
|
||||||
|
```bash
|
||||||
|
cd /home/liq/gitProjects/simple-notes-sync/android
|
||||||
|
./scripts/create-keystore.fish
|
||||||
|
```
|
||||||
|
|
||||||
|
**Das Script:**
|
||||||
|
1. Erstellt einen neuen 4096-Bit RSA-Keystore
|
||||||
|
2. Generiert `app/simple-notes-release.jks`
|
||||||
|
3. Erstellt `key.properties` mit den Zugangsdaten
|
||||||
|
4. Zeigt Base64-kodierten Keystore für GitHub Secrets
|
||||||
|
5. Gibt SHA256-Fingerprint zur Verifikation aus
|
||||||
|
|
||||||
|
**Output:**
|
||||||
|
- ✅ `app/simple-notes-release.jks` - Der Keystore
|
||||||
|
- ✅ `key.properties` - Lokale Signing-Konfiguration
|
||||||
|
- 📋 GitHub Secrets zum Kopieren
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. `verify-secrets.fish` - GitHub Secrets & Keystore verifizieren
|
||||||
|
|
||||||
|
**Wann verwenden:**
|
||||||
|
- ✅ Nach `create-keystore.fish` zur Verifikation
|
||||||
|
- ✅ Vor einem Release-Build zum Troubleshooting
|
||||||
|
- ✅ Um zu prüfen ob alles korrekt konfiguriert ist
|
||||||
|
|
||||||
|
**Verwendung:**
|
||||||
|
```bash
|
||||||
|
cd /home/liq/gitProjects/simple-notes-sync/android
|
||||||
|
./scripts/verify-secrets.fish
|
||||||
|
```
|
||||||
|
|
||||||
|
**Das Script prüft:**
|
||||||
|
- GitHub CLI Installation & Authentifizierung
|
||||||
|
- Ob alle 4 erforderlichen GitHub Secrets gesetzt sind
|
||||||
|
- Ob `key.properties` lokal existiert
|
||||||
|
- Ob der Keystore existiert
|
||||||
|
- Zeigt SHA256-Fingerprint des Keystores
|
||||||
|
|
||||||
|
**Output:**
|
||||||
|
- ✅ Status aller Secrets
|
||||||
|
- ✅ Status der lokalen Konfiguration
|
||||||
|
- 💡 Empfehlungen bei Problemen
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. `build-release-local.fish` - Lokal signierte Release-APKs bauen
|
||||||
|
|
||||||
|
**Wann verwenden:**
|
||||||
|
- ✅ Lokale Test-APKs erstellen, die mit Releases kompatibel sind
|
||||||
|
- ✅ APKs vor dem GitHub Release testen
|
||||||
|
- ✅ Schneller als GitHub Actions für Tests
|
||||||
|
|
||||||
|
**Voraussetzung:**
|
||||||
|
- `key.properties` muss existieren (via `create-keystore.fish` erstellt)
|
||||||
|
|
||||||
|
**Verwendung:**
|
||||||
|
```bash
|
||||||
|
cd /home/liq/gitProjects/simple-notes-sync/android
|
||||||
|
./scripts/build-release-local.fish
|
||||||
|
```
|
||||||
|
|
||||||
|
**Interaktive Auswahl:**
|
||||||
|
1. Standard Flavor (empfohlen)
|
||||||
|
2. F-Droid Flavor
|
||||||
|
3. Beide Flavors
|
||||||
|
|
||||||
|
**Output:**
|
||||||
|
- `app/build/outputs/apk/standard/release/` - Signierte Standard APKs
|
||||||
|
- `app/build/outputs/apk/fdroid/release/` - Signierte F-Droid APKs
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Kompletter Workflow (von 0 auf Release)
|
||||||
|
|
||||||
|
### Erstmaliges Setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /home/liq/gitProjects/simple-notes-sync/android
|
||||||
|
|
||||||
|
# 1. Keystore erstellen (mit automatischer GitHub Secrets-Konfiguration!)
|
||||||
|
./scripts/create-keystore.fish
|
||||||
|
# → Folge den Anweisungen, speichere die Passwörter!
|
||||||
|
# → GitHub Secrets werden automatisch via GitHub CLI gesetzt
|
||||||
|
|
||||||
|
# 2. Verifiziere die Konfiguration
|
||||||
|
./scripts/verify-secrets.fish
|
||||||
|
# → Prüft ob alle Secrets gesetzt sind
|
||||||
|
# → Zeigt Keystore-Informationen
|
||||||
|
|
||||||
|
# 3. Teste lokalen Build
|
||||||
|
./scripts/build-release-local.fish
|
||||||
|
# → Wähle "1" für Standard Flavor
|
||||||
|
|
||||||
|
# 4. Verifiziere Signatur
|
||||||
|
keytool -printcert -jarfile app/build/outputs/apk/standard/release/app-standard-universal-release.apk
|
||||||
|
```
|
||||||
|
|
||||||
|
### Vor jedem Release
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Code committen und pushen
|
||||||
|
git add .
|
||||||
|
git commit -m "✨ Neue Features"
|
||||||
|
git push origin main
|
||||||
|
|
||||||
|
# 2. GitHub Actions erstellt automatisch Release
|
||||||
|
# → Workflow läuft: .github/workflows/build-production-apk.yml
|
||||||
|
# → Erstellt Release mit signierten APKs
|
||||||
|
|
||||||
|
# Optional: Lokalen Test-Build vorher
|
||||||
|
./scripts/build-release-local.fish
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔐 Sicherheitshinweise
|
||||||
|
|
||||||
|
### ⚠️ Diese Dateien NIEMALS committen:
|
||||||
|
- `key.properties` - Enthält Keystore-Passwörter
|
||||||
|
- `*.jks` / `*.keystore` - Der Keystore selbst
|
||||||
|
- Beide sind bereits in `.gitignore`
|
||||||
|
|
||||||
|
### ✅ Diese Werte sicher speichern:
|
||||||
|
- Keystore-Passwort
|
||||||
|
- Key-Alias
|
||||||
|
- Key-Passwort
|
||||||
|
- Base64-kodierter Keystore (für GitHub Secrets)
|
||||||
|
|
||||||
|
**Empfehlung:** Nutze einen Passwort-Manager (Bitwarden, 1Password, etc.)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛠️ Troubleshooting
|
||||||
|
|
||||||
|
### "Keystore not found"
|
||||||
|
```bash
|
||||||
|
# Prüfe ob Keystore existiert
|
||||||
|
ls -la app/simple-notes-release.jks
|
||||||
|
|
||||||
|
# Falls nicht: Neu erstellen
|
||||||
|
./scripts/create-keystore.fish
|
||||||
|
```
|
||||||
|
|
||||||
|
### "key.properties not found"
|
||||||
|
```bash
|
||||||
|
# Prüfe ob Datei existiert
|
||||||
|
ls -la key.properties
|
||||||
|
|
||||||
|
# Falls nicht: Keystore neu erstellen oder manuell anlegen
|
||||||
|
./scripts/create-keystore.fish
|
||||||
|
```
|
||||||
|
|
||||||
|
### "Signature mismatch" beim App-Update
|
||||||
|
**Problem:** Lokaler Build hat andere Signatur als GitHub Release
|
||||||
|
|
||||||
|
**Ursache:** Unterschiedliche Keystores oder Passwörter
|
||||||
|
|
||||||
|
**Lösung:**
|
||||||
|
1. Vergleiche SHA256-Fingerprints:
|
||||||
|
```bash
|
||||||
|
# Lokal
|
||||||
|
keytool -list -v -keystore app/simple-notes-release.jks
|
||||||
|
|
||||||
|
# GitHub Release-APK
|
||||||
|
keytool -printcert -jarfile ~/Downloads/simple-notes-v1.1.0.apk
|
||||||
|
```
|
||||||
|
2. Müssen **identisch** sein!
|
||||||
|
3. Falls nicht: GitHub Secrets mit lokaler `key.properties` synchronisieren
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 Weitere Dokumentation
|
||||||
|
|
||||||
|
- `../LOCAL_BUILDS.md` - Detaillierte Anleitung für lokale Builds
|
||||||
|
- `../.github/workflows/build-production-apk.yml` - GitHub Actions Workflow
|
||||||
|
- `../app/build.gradle.kts` - Build-Konfiguration
|
||||||
104
android/scripts/build-release-local.fish
Executable file
104
android/scripts/build-release-local.fish
Executable file
@@ -0,0 +1,104 @@
|
|||||||
|
#!/usr/bin/env fish
|
||||||
|
|
||||||
|
# Simple Notes Sync - Lokaler Release Build
|
||||||
|
# Erstellt signierte APKs, die mit GitHub Release-APKs kompatibel sind
|
||||||
|
|
||||||
|
set -l SCRIPT_DIR (dirname (status --current-filename))
|
||||||
|
set -l ANDROID_DIR (realpath "$SCRIPT_DIR/..")
|
||||||
|
set -l KEY_PROPERTIES "$ANDROID_DIR/key.properties"
|
||||||
|
|
||||||
|
echo "🔨 Simple Notes Sync - Lokaler Release Build"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 1. Prüfe ob key.properties existiert
|
||||||
|
if not test -f "$KEY_PROPERTIES"
|
||||||
|
echo "❌ Fehler: key.properties nicht gefunden!"
|
||||||
|
echo ""
|
||||||
|
echo "Bitte erstelle die Datei:"
|
||||||
|
echo " cd $ANDROID_DIR"
|
||||||
|
echo " cp key.properties.example key.properties"
|
||||||
|
echo ""
|
||||||
|
echo "Und fülle sie mit den echten Keystore-Daten aus."
|
||||||
|
echo "Siehe: android/LOCAL_BUILDS.md"
|
||||||
|
exit 1
|
||||||
|
end
|
||||||
|
|
||||||
|
# 2. Prüfe ob Keystore existiert
|
||||||
|
set -l KEYSTORE "$ANDROID_DIR/app/simple-notes-release.jks"
|
||||||
|
if not test -f "$KEYSTORE"
|
||||||
|
echo "❌ Fehler: Keystore nicht gefunden!"
|
||||||
|
echo " Erwartet: $KEYSTORE"
|
||||||
|
exit 1
|
||||||
|
end
|
||||||
|
|
||||||
|
echo "✅ key.properties gefunden"
|
||||||
|
echo "✅ Keystore gefunden"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 3. Build-Typ abfragen
|
||||||
|
echo "Welchen Build möchtest du erstellen?"
|
||||||
|
echo " 1) Standard Flavor (empfohlen)"
|
||||||
|
echo " 2) F-Droid Flavor"
|
||||||
|
echo " 3) Beide Flavors"
|
||||||
|
echo ""
|
||||||
|
read -P "Auswahl [1-3]: " -n 1 choice
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
switch $choice
|
||||||
|
case 1
|
||||||
|
echo "🏗️ Baue Standard Release APKs..."
|
||||||
|
cd "$ANDROID_DIR"
|
||||||
|
./gradlew assembleStandardRelease --no-daemon
|
||||||
|
|
||||||
|
if test $status -eq 0
|
||||||
|
echo ""
|
||||||
|
echo "✅ Build erfolgreich!"
|
||||||
|
echo ""
|
||||||
|
echo "📦 APKs findest du hier:"
|
||||||
|
echo " $ANDROID_DIR/app/build/outputs/apk/standard/release/"
|
||||||
|
ls -lh "$ANDROID_DIR/app/build/outputs/apk/standard/release/"*.apk
|
||||||
|
end
|
||||||
|
|
||||||
|
case 2
|
||||||
|
echo "🏗️ Baue F-Droid Release APKs..."
|
||||||
|
cd "$ANDROID_DIR"
|
||||||
|
./gradlew assembleFdroidRelease --no-daemon
|
||||||
|
|
||||||
|
if test $status -eq 0
|
||||||
|
echo ""
|
||||||
|
echo "✅ Build erfolgreich!"
|
||||||
|
echo ""
|
||||||
|
echo "📦 APKs findest du hier:"
|
||||||
|
echo " $ANDROID_DIR/app/build/outputs/apk/fdroid/release/"
|
||||||
|
ls -lh "$ANDROID_DIR/app/build/outputs/apk/fdroid/release/"*.apk
|
||||||
|
end
|
||||||
|
|
||||||
|
case 3
|
||||||
|
echo "🏗️ Baue Standard + F-Droid Release APKs..."
|
||||||
|
cd "$ANDROID_DIR"
|
||||||
|
./gradlew assembleStandardRelease assembleFdroidRelease --no-daemon
|
||||||
|
|
||||||
|
if test $status -eq 0
|
||||||
|
echo ""
|
||||||
|
echo "✅ Build erfolgreich!"
|
||||||
|
echo ""
|
||||||
|
echo "📦 Standard APKs:"
|
||||||
|
echo " $ANDROID_DIR/app/build/outputs/apk/standard/release/"
|
||||||
|
ls -lh "$ANDROID_DIR/app/build/outputs/apk/standard/release/"*.apk
|
||||||
|
echo ""
|
||||||
|
echo "📦 F-Droid APKs:"
|
||||||
|
echo " $ANDROID_DIR/app/build/outputs/apk/fdroid/release/"
|
||||||
|
ls -lh "$ANDROID_DIR/app/build/outputs/apk/fdroid/release/"*.apk
|
||||||
|
end
|
||||||
|
|
||||||
|
case '*'
|
||||||
|
echo "❌ Ungültige Auswahl"
|
||||||
|
exit 1
|
||||||
|
end
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "💡 Tipp: Du kannst die APK per HTTP Server verteilen:"
|
||||||
|
echo " cd app/build/outputs/apk/standard/release"
|
||||||
|
echo " python3 -m http.server 8892"
|
||||||
279
android/scripts/create-keystore.fish
Executable file
279
android/scripts/create-keystore.fish
Executable file
@@ -0,0 +1,279 @@
|
|||||||
|
#!/usr/bin/env fish
|
||||||
|
|
||||||
|
# Simple Notes Sync - Keystore Generator
|
||||||
|
# Erstellt einen neuen Release-Keystore für App-Signierung
|
||||||
|
|
||||||
|
set -l SCRIPT_DIR (dirname (status --current-filename))
|
||||||
|
set -l ANDROID_DIR (realpath "$SCRIPT_DIR/..")
|
||||||
|
set -l KEYSTORE_PATH "$ANDROID_DIR/app/simple-notes-release.jks"
|
||||||
|
set -l KEY_PROPERTIES "$ANDROID_DIR/key.properties"
|
||||||
|
|
||||||
|
echo "🔐 Simple Notes Sync - Keystore Generator"
|
||||||
|
echo ""
|
||||||
|
echo "⚠️ WICHTIG: Dieser Keystore wird für alle zukünftigen App-Releases verwendet!"
|
||||||
|
echo " Speichere die Zugangsdaten sicher ab (z.B. in einem Passwort-Manager)."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Prüfe ob Keystore bereits existiert
|
||||||
|
if test -f "$KEYSTORE_PATH"
|
||||||
|
echo "⚠️ Ein Keystore existiert bereits:"
|
||||||
|
echo " $KEYSTORE_PATH"
|
||||||
|
echo ""
|
||||||
|
read -P "Möchtest du ihn überschreiben? (Dies macht alte APKs inkompatibel!) [j/N]: " -n 1 overwrite
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
if not string match -qi "j" $overwrite
|
||||||
|
echo "❌ Abgebrochen."
|
||||||
|
exit 1
|
||||||
|
end
|
||||||
|
|
||||||
|
echo "🗑️ Lösche alten Keystore..."
|
||||||
|
rm "$KEYSTORE_PATH"
|
||||||
|
end
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "📝 Bitte gib die folgenden Informationen ein:"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# App-Informationen sammeln
|
||||||
|
read -P "Dein Name (z.B. 'Max Mustermann'): " developer_name
|
||||||
|
read -P "Organisation (z.B. 'dettmer.dev'): " organization
|
||||||
|
read -P "Stadt: " city
|
||||||
|
read -P "Land (z.B. 'DE'): " country
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "🔒 Keystore-Passwörter:"
|
||||||
|
echo ""
|
||||||
|
echo "Möchtest du sichere Passwörter automatisch generieren lassen?"
|
||||||
|
read -P "[J/n]: " -n 1 auto_generate
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
if string match -qi "n" $auto_generate
|
||||||
|
# Manuelle Passwort-Eingabe
|
||||||
|
echo ""
|
||||||
|
echo "📝 Manuelle Passwort-Eingabe:"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
while true
|
||||||
|
read -sP "Keystore-Passwort: " keystore_password
|
||||||
|
echo ""
|
||||||
|
read -sP "Keystore-Passwort (Bestätigung): " keystore_password_confirm
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
if test "$keystore_password" = "$keystore_password_confirm"
|
||||||
|
break
|
||||||
|
else
|
||||||
|
echo "❌ Passwörter stimmen nicht überein. Bitte erneut eingeben."
|
||||||
|
echo ""
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
while true
|
||||||
|
read -sP "Key-Passwort: " key_password
|
||||||
|
echo ""
|
||||||
|
read -sP "Key-Passwort (Bestätigung): " key_password_confirm
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
if test "$key_password" = "$key_password_confirm"
|
||||||
|
break
|
||||||
|
else
|
||||||
|
echo "❌ Passwörter stimmen nicht überein. Bitte erneut eingeben."
|
||||||
|
echo ""
|
||||||
|
end
|
||||||
|
end
|
||||||
|
else
|
||||||
|
# Automatische Passwort-Generierung
|
||||||
|
echo ""
|
||||||
|
echo "🔐 Generiere sichere Passwörter (32 Zeichen, alphanumerisch)..."
|
||||||
|
|
||||||
|
# Generiere sichere, zufällige Passwörter (alphanumerisch, 32 Zeichen)
|
||||||
|
set keystore_password (openssl rand -base64 32 | tr -d '/+=' | head -c 32)
|
||||||
|
set key_password (openssl rand -base64 32 | tr -d '/+=' | head -c 32)
|
||||||
|
|
||||||
|
echo "✅ Passwörter generiert"
|
||||||
|
echo ""
|
||||||
|
echo "⚠️ WICHTIG: Speichere diese Passwörter jetzt in deinem Passwort-Manager!"
|
||||||
|
echo ""
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
echo "Keystore-Passwort: $keystore_password"
|
||||||
|
echo "Key-Passwort: $key_password"
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
echo ""
|
||||||
|
read -P "Passwörter gespeichert? Drücke Enter zum Fortfahren..."
|
||||||
|
end
|
||||||
|
|
||||||
|
set -l key_alias "simple-notes-key"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "🏗️ Erstelle Keystore..."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Keystore erstellen
|
||||||
|
keytool -genkey \
|
||||||
|
-v \
|
||||||
|
-keystore "$KEYSTORE_PATH" \
|
||||||
|
-alias "$key_alias" \
|
||||||
|
-keyalg RSA \
|
||||||
|
-keysize 4096 \
|
||||||
|
-validity 10000 \
|
||||||
|
-storepass "$keystore_password" \
|
||||||
|
-keypass "$key_password" \
|
||||||
|
-dname "CN=$developer_name, OU=Simple Notes Sync, O=$organization, L=$city, C=$country"
|
||||||
|
|
||||||
|
if test $status -ne 0
|
||||||
|
echo ""
|
||||||
|
echo "❌ Fehler beim Erstellen des Keystores!"
|
||||||
|
exit 1
|
||||||
|
end
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "✅ Keystore erfolgreich erstellt!"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# key.properties erstellen
|
||||||
|
echo "📝 Erstelle key.properties..."
|
||||||
|
echo "storeFile=simple-notes-release.jks" > "$KEY_PROPERTIES"
|
||||||
|
echo "storePassword=$keystore_password" >> "$KEY_PROPERTIES"
|
||||||
|
echo "keyAlias=$key_alias" >> "$KEY_PROPERTIES"
|
||||||
|
echo "keyPassword=$key_password" >> "$KEY_PROPERTIES"
|
||||||
|
|
||||||
|
echo "✅ key.properties erstellt"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Keystore-Info anzeigen
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
echo "📋 KEYSTORE-INFORMATIONEN - SICHER SPEICHERN!"
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
echo ""
|
||||||
|
echo "Keystore-Pfad: $KEYSTORE_PATH"
|
||||||
|
echo "Key-Alias: $key_alias"
|
||||||
|
echo "Keystore-Passwort: $keystore_password"
|
||||||
|
echo "Key-Passwort: $key_password"
|
||||||
|
echo ""
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Base64-kodierten Keystore für GitHub Secrets
|
||||||
|
echo "🔐 Base64-kodierter Keystore für GitHub Secrets:"
|
||||||
|
echo ""
|
||||||
|
set -l keystore_base64 (base64 -w 0 "$KEYSTORE_PATH")
|
||||||
|
echo "$keystore_base64"
|
||||||
|
echo ""
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# GitHub Secrets konfigurieren
|
||||||
|
echo "<22> GitHub Secrets konfigurieren..."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Prüfe ob GitHub CLI installiert ist
|
||||||
|
if not command -v gh &> /dev/null
|
||||||
|
echo "⚠️ GitHub CLI (gh) nicht gefunden!"
|
||||||
|
echo ""
|
||||||
|
echo "📝 Manuelle Konfiguration erforderlich:"
|
||||||
|
echo ""
|
||||||
|
echo "1. Gehe zu: https://github.com/inentory69/simple-notes-sync/settings/secrets/actions"
|
||||||
|
echo "2. Erstelle/Aktualisiere folgende Secrets:"
|
||||||
|
echo ""
|
||||||
|
echo " KEYSTORE_BASE64:"
|
||||||
|
echo " $keystore_base64"
|
||||||
|
echo ""
|
||||||
|
echo " KEYSTORE_PASSWORD:"
|
||||||
|
echo " $keystore_password"
|
||||||
|
echo ""
|
||||||
|
echo " KEY_ALIAS:"
|
||||||
|
echo " $key_alias"
|
||||||
|
echo ""
|
||||||
|
echo " KEY_PASSWORD:"
|
||||||
|
echo " $key_password"
|
||||||
|
echo ""
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
echo ""
|
||||||
|
else
|
||||||
|
# GitHub CLI verfügbar - automatisch Secrets erstellen
|
||||||
|
echo "✅ GitHub CLI gefunden"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Prüfe ob authentifiziert
|
||||||
|
if not gh auth status &> /dev/null
|
||||||
|
echo "⚠️ Nicht bei GitHub authentifiziert!"
|
||||||
|
echo ""
|
||||||
|
read -P "Möchtest du dich jetzt authentifizieren? [j/N]: " -n 1 do_auth
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
if string match -qi "j" $do_auth
|
||||||
|
gh auth login
|
||||||
|
else
|
||||||
|
echo "❌ Überspringe automatische Secret-Konfiguration"
|
||||||
|
echo ""
|
||||||
|
echo "📝 Manuelle Konfiguration erforderlich (siehe oben)"
|
||||||
|
echo ""
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
echo ""
|
||||||
|
return
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
echo "🔐 Erstelle/Aktualisiere GitHub Secrets..."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
set -l repo "inventory69/simple-notes-sync"
|
||||||
|
|
||||||
|
# KEYSTORE_BASE64
|
||||||
|
echo "$keystore_base64" | gh secret set KEYSTORE_BASE64 --repo $repo
|
||||||
|
if test $status -eq 0
|
||||||
|
echo "✅ KEYSTORE_BASE64 gesetzt"
|
||||||
|
else
|
||||||
|
echo "❌ Fehler beim Setzen von KEYSTORE_BASE64"
|
||||||
|
end
|
||||||
|
|
||||||
|
# KEYSTORE_PASSWORD
|
||||||
|
echo "$keystore_password" | gh secret set KEYSTORE_PASSWORD --repo $repo
|
||||||
|
if test $status -eq 0
|
||||||
|
echo "✅ KEYSTORE_PASSWORD gesetzt"
|
||||||
|
else
|
||||||
|
echo "❌ Fehler beim Setzen von KEYSTORE_PASSWORD"
|
||||||
|
end
|
||||||
|
|
||||||
|
# KEY_ALIAS
|
||||||
|
echo "$key_alias" | gh secret set KEY_ALIAS --repo $repo
|
||||||
|
if test $status -eq 0
|
||||||
|
echo "✅ KEY_ALIAS gesetzt"
|
||||||
|
else
|
||||||
|
echo "❌ Fehler beim Setzen von KEY_ALIAS"
|
||||||
|
end
|
||||||
|
|
||||||
|
# KEY_PASSWORD
|
||||||
|
echo "$key_password" | gh secret set KEY_PASSWORD --repo $repo
|
||||||
|
if test $status -eq 0
|
||||||
|
echo "✅ KEY_PASSWORD gesetzt"
|
||||||
|
else
|
||||||
|
echo "❌ Fehler beim Setzen von KEY_PASSWORD"
|
||||||
|
end
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
echo ""
|
||||||
|
echo "✅ GitHub Secrets erfolgreich konfiguriert!"
|
||||||
|
echo ""
|
||||||
|
echo "🔍 Verifizieren:"
|
||||||
|
echo " gh secret list --repo $repo"
|
||||||
|
echo ""
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
echo ""
|
||||||
|
end
|
||||||
|
|
||||||
|
# Signatur-Fingerprint anzeigen
|
||||||
|
echo "🔑 SHA256-Fingerprint (zur Verifikation):"
|
||||||
|
keytool -list -v -keystore "$KEYSTORE_PATH" -storepass "$keystore_password" | grep "SHA256:"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "✅ Setup abgeschlossen!"
|
||||||
|
echo ""
|
||||||
|
echo "💡 Nächste Schritte:"
|
||||||
|
echo " 1. Speichere die obigen Informationen in einem Passwort-Manager"
|
||||||
|
echo " 2. Konfiguriere die GitHub Secrets (siehe oben)"
|
||||||
|
echo " 3. Teste den lokalen Build:"
|
||||||
|
echo " cd $ANDROID_DIR"
|
||||||
|
echo " ./gradlew assembleStandardRelease"
|
||||||
|
echo ""
|
||||||
150
android/scripts/verify-secrets.fish
Executable file
150
android/scripts/verify-secrets.fish
Executable file
@@ -0,0 +1,150 @@
|
|||||||
|
#!/usr/bin/env fish
|
||||||
|
|
||||||
|
# Simple Notes Sync - GitHub Secrets Verifier
|
||||||
|
# Verifiziert ob die GitHub Secrets korrekt konfiguriert sind
|
||||||
|
|
||||||
|
set -l repo "inventory69/simple-notes-sync"
|
||||||
|
|
||||||
|
echo "🔍 GitHub Secrets Verifier"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Prüfe ob GitHub CLI installiert ist
|
||||||
|
if not command -v gh &> /dev/null
|
||||||
|
echo "❌ GitHub CLI (gh) nicht gefunden!"
|
||||||
|
echo ""
|
||||||
|
echo "Installation:"
|
||||||
|
echo " Arch Linux: sudo pacman -S github-cli"
|
||||||
|
echo " Ubuntu: sudo apt install gh"
|
||||||
|
echo " macOS: brew install gh"
|
||||||
|
echo ""
|
||||||
|
exit 1
|
||||||
|
end
|
||||||
|
|
||||||
|
# Prüfe Authentifizierung
|
||||||
|
if not gh auth status &> /dev/null
|
||||||
|
echo "❌ Nicht bei GitHub authentifiziert!"
|
||||||
|
echo ""
|
||||||
|
echo "Authentifizierung starten:"
|
||||||
|
echo " gh auth login"
|
||||||
|
echo ""
|
||||||
|
exit 1
|
||||||
|
end
|
||||||
|
|
||||||
|
echo "✅ GitHub CLI authentifiziert"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Liste alle Secrets auf
|
||||||
|
echo "📋 Konfigurierte Secrets für $repo:"
|
||||||
|
echo ""
|
||||||
|
gh secret list --repo $repo
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Prüfe ob alle erforderlichen Secrets vorhanden sind
|
||||||
|
set -l required_secrets "KEYSTORE_BASE64" "KEYSTORE_PASSWORD" "KEY_ALIAS" "KEY_PASSWORD"
|
||||||
|
set -l missing_secrets
|
||||||
|
|
||||||
|
for secret in $required_secrets
|
||||||
|
if not gh secret list --repo $repo | grep -q "^$secret"
|
||||||
|
set -a missing_secrets $secret
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if test (count $missing_secrets) -gt 0
|
||||||
|
echo "❌ Fehlende Secrets:"
|
||||||
|
for secret in $missing_secrets
|
||||||
|
echo " - $secret"
|
||||||
|
end
|
||||||
|
echo ""
|
||||||
|
echo "💡 Tipp: Führe create-keystore.fish aus, um die Secrets zu erstellen"
|
||||||
|
else
|
||||||
|
echo "✅ Alle erforderlichen Secrets sind konfiguriert!"
|
||||||
|
echo ""
|
||||||
|
echo "Required Secrets:"
|
||||||
|
for secret in $required_secrets
|
||||||
|
echo " ✓ $secret"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Prüfe ob key.properties lokal existiert
|
||||||
|
set -l SCRIPT_DIR (dirname (status --current-filename))
|
||||||
|
set -l ANDROID_DIR (realpath "$SCRIPT_DIR/..")
|
||||||
|
set -l KEY_PROPERTIES "$ANDROID_DIR/key.properties"
|
||||||
|
|
||||||
|
if test -f "$KEY_PROPERTIES"
|
||||||
|
echo "✅ Lokale key.properties gefunden: $KEY_PROPERTIES"
|
||||||
|
echo ""
|
||||||
|
echo "📋 Inhalt (Passwörter verborgen):"
|
||||||
|
cat "$KEY_PROPERTIES" | sed 's/\(Password=\).*/\1***HIDDEN***/g'
|
||||||
|
else
|
||||||
|
echo "⚠️ Lokale key.properties nicht gefunden: $KEY_PROPERTIES"
|
||||||
|
echo ""
|
||||||
|
echo "💡 Tipp: Führe create-keystore.fish aus, um sie zu erstellen"
|
||||||
|
end
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Prüfe ob Keystore existiert
|
||||||
|
set -l KEYSTORE "$ANDROID_DIR/app/simple-notes-release.jks"
|
||||||
|
if test -f "$KEYSTORE"
|
||||||
|
echo "✅ Keystore gefunden: $KEYSTORE"
|
||||||
|
|
||||||
|
# Zeige Keystore-Info (wenn key.properties existiert)
|
||||||
|
if test -f "$KEY_PROPERTIES"
|
||||||
|
set -l store_password (grep "storePassword=" "$KEY_PROPERTIES" | cut -d'=' -f2)
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "🔑 Keystore-Informationen:"
|
||||||
|
keytool -list -v -keystore "$KEYSTORE" -storepass "$store_password" 2>/dev/null | grep -E "(Alias|Creation date|Valid|SHA256)" | head -10
|
||||||
|
end
|
||||||
|
else
|
||||||
|
echo "⚠️ Keystore nicht gefunden: $KEYSTORE"
|
||||||
|
echo ""
|
||||||
|
echo "💡 Tipp: Führe create-keystore.fish aus, um ihn zu erstellen"
|
||||||
|
end
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Zusammenfassung
|
||||||
|
set -l issues 0
|
||||||
|
|
||||||
|
if test (count $missing_secrets) -gt 0
|
||||||
|
set issues (math $issues + 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
if not test -f "$KEY_PROPERTIES"
|
||||||
|
set issues (math $issues + 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
if not test -f "$KEYSTORE"
|
||||||
|
set issues (math $issues + 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
if test $issues -eq 0
|
||||||
|
echo "🎉 Alles konfiguriert! Du kannst jetzt Releases erstellen."
|
||||||
|
echo ""
|
||||||
|
echo "🚀 Nächste Schritte:"
|
||||||
|
echo " 1. Lokalen Build testen:"
|
||||||
|
echo " ./scripts/build-release-local.fish"
|
||||||
|
echo ""
|
||||||
|
echo " 2. Code committen und pushen:"
|
||||||
|
echo " git push origin main"
|
||||||
|
echo ""
|
||||||
|
echo " 3. GitHub Actions erstellt automatisch Release"
|
||||||
|
else
|
||||||
|
echo "⚠️ $issues Problem(e) gefunden - siehe oben"
|
||||||
|
echo ""
|
||||||
|
echo "💡 Lösung: Führe create-keystore.fish aus"
|
||||||
|
end
|
||||||
|
|
||||||
|
echo ""
|
||||||
Reference in New Issue
Block a user