chahinebrini b31066a04c feat(chat): native action sheet + Insta-style heart for DM messages
- ChatBubble: useActionSheet replaces custom Modal (native iOS popup, Android bottom sheet)
- DM mode (isDM prop): hides like-count, shows Insta-style heart badge under bubble when liked
- Group chat unchanged
- Cleanup: remove unused Modal/Platform imports, sheet styles, actionsOpen state
- deploy.sh: auto-detect ANDROID_HOME + auto-create local.properties for local Gradle
- NEXT_RELEASE.md: DM reactions release note
- Includes other staged work across binder-mac, marketing, ops/mdm, ios/
2026-05-30 09:14:32 +02:00

829 lines
30 KiB
Bash
Executable File

#!/bin/bash
# deploy.sh — ReBreak Multi-Platform Release Pipeline
#
# SUBCOMMANDS:
# ./deploy.sh default: all (testflight + mdm + android)
# ./deploy.sh testflight iOS TestFlight via App Store Connect
# ./deploy.sh mdm iOS Ad-Hoc IPA + scp Upload zu MDM-Server
# ./deploy.sh android Android APK/AAB via Gradle + Play Console
# ./deploy.sh all Alle drei Targets
#
# FLAGS:
# --no-bump Build-Number NICHT bumpen (Default: bump an)
# --version X.Y.Z Explizite Version setzen
# --build N Explizite iOS Build-Nummer
# --android-version-code N Override Android versionCode
# --notes "text" Release-Notes für diese Version (TestFlight + Play Console)
# --skip-clean clean-ios.sh überspringen (iOS)
# --skip-validate altool --validate-app überspringen (TF)
# --skip-submit Play-Console-Submit überspringen (Android)
# --keep-build Build-Artefakte NICHT löschen (Default: cleanup nach Submit)
# --dry-run Alles simulieren, nichts ausführen
# -h, --help Diese Hilfe anzeigen
#
# BEISPIELE:
# # Full Release (alle Plattformen — bumpt + cleanup automatisch):
# ./deploy.sh
#
# # Nur Android build (kein Submit, Build behalten):
# ./deploy.sh android --skip-submit --keep-build
#
# # Nur iOS TestFlight mit expliziter Version (ohne Bump):
# ./deploy.sh testflight --no-bump --version 0.4.0 --build 26
#
# # Dry-Run zum Testen:
# ./deploy.sh all --dry-run
#
# CREDENTIALS:
# iOS TestFlight:
# - APPLE_APP_SPECIFIC_PASSWORD (oder)
# - ASC_API_KEY_PATH + ASC_API_KEY_ID + ASC_API_KEY_ISSUER
# iOS MDM:
# - SSH-Access zu rebreak-mdm Server
# Android:
# - android/key.properties (signing)
# - android/app/*.keystore (release keystore)
# - PLAY_SERVICE_ACCOUNT_JSON (für --submit)
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
APP_CONFIG="$SCRIPT_DIR/app.config.ts"
PACKAGE_JSON="$SCRIPT_DIR/package.json"
LOG_DIR="$SCRIPT_DIR/tmp/deploy-logs"
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
# ═══════════════════════════════════════════════════════════════════════════
# Color Output (brew-style)
# ═══════════════════════════════════════════════════════════════════════════
if [[ -t 1 ]]; then
BOLD=$(tput bold)
GREEN=$(tput setaf 2)
YELLOW=$(tput setaf 3)
RED=$(tput setaf 1)
BLUE=$(tput setaf 4)
RESET=$(tput sgr0)
else
BOLD="" GREEN="" YELLOW="" RED="" BLUE="" RESET=""
fi
log() { echo "${BLUE}==>${RESET} ${BOLD}$*${RESET}"; }
ok() { echo "${GREEN}${RESET} $*"; }
warn() { echo "${YELLOW}${RESET} $*" >&2; }
error() { echo "${RED}${RESET} ${BOLD}$*${RESET}" >&2; }
die() { error "$*"; exit 1; }
section() {
echo ""
echo "${BOLD}────────────────────────────────────────────────────────────${RESET}"
echo "${BOLD}$*${RESET}"
echo "${BOLD}────────────────────────────────────────────────────────────${RESET}"
}
run() {
if $DRY_RUN; then
echo "${YELLOW}[DRY-RUN]${RESET} $*"
return 0
else
"$@"
fi
}
# run_quiet "Label" <log-file> <cmd...>
# Runs cmd silently with a spinner + elapsed time. On error dumps last 40 log
# lines and exits. With --verbose / non-TTY: streams full output normally.
run_quiet() {
local label="$1"; shift
local logfile="$1"; shift
if $DRY_RUN; then
echo "${YELLOW}[DRY-RUN]${RESET} $label: $*"
return 0
fi
if $VERBOSE || [[ ! -t 1 ]]; then
log "$label"
"$@" 2>&1 | tee "$logfile"
return ${PIPESTATUS[0]}
fi
local start=$SECONDS
local spin='⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏'
local i=0 pid elapsed frame
( "$@" >"$logfile" 2>&1 ) &
pid=$!
while kill -0 "$pid" 2>/dev/null; do
elapsed=$((SECONDS - start))
frame="${spin:i%10:1}"
i=$((i + 1))
# \r + \033[K = carriage return + clear line to end
printf '\r\033[K%s %s==>%s %s %s(%ds)%s' \
"$frame" "$BLUE" "$RESET" "$label" "$YELLOW" "$elapsed" "$RESET" >&2
sleep 0.1
done
wait "$pid"
local rc=$?
elapsed=$((SECONDS - start))
printf '\r\033[K' >&2 # Clear spinner line
if [[ $rc -eq 0 ]]; then
ok "$label ${YELLOW}(${elapsed}s)${RESET}"
else
error "$label fehlgeschlagen nach ${elapsed}s (exit $rc)"
echo "" >&2
echo "${BOLD}── Letzte Log-Zeilen (${logfile}) ──${RESET}" >&2
tail -40 "$logfile" >&2
echo "${BOLD}────────────────────────────────────${RESET}" >&2
echo "Voller Log: $logfile" >&2
exit $rc
fi
}
# ═══════════════════════════════════════════════════════════════════════════
# Flag Parsing
# ═══════════════════════════════════════════════════════════════════════════
COMMAND="${1:-all}"
shift || true
DO_MDM=false
DO_TF=false
DO_ANDROID=false
case "$COMMAND" in
all) DO_MDM=true; DO_TF=true; DO_ANDROID=true ;;
testflight|tf) DO_TF=true ;;
mdm|adhoc) DO_MDM=true ;;
android) DO_ANDROID=true ;;
-h|--help)
awk '/^#!/{next} /^#/{sub(/^# ?/, ""); print; next} {exit}' "$0"
exit 0 ;;
*)
error "Unbekanntes Subcommand: $COMMAND"
echo ""
echo "Verfügbare Commands:"
echo " all Alle Plattformen (testflight + mdm + android)"
echo " testflight Nur iOS TestFlight"
echo " mdm Nur iOS Ad-Hoc/MDM"
echo " android Nur Android"
echo ""
echo "Nutze --help für Details"
exit 1
;;
esac
# Bump default ON — Android requires new versionCode for every upload,
# TestFlight requires unique build per version. --no-bump to opt out.
BUMP_IOS=true
BUMP_ANDROID=true
EXPLICIT_VERSION=""
EXPLICIT_BUILD=""
ANDROID_VERSION_CODE_OVERRIDE=""
RELEASE_NOTES=""
SKIP_CLEAN=false
SKIP_VALIDATE=false
SKIP_SUBMIT=false
KEEP_BUILD=false
DRY_RUN=false
VERBOSE=false
while [[ $# -gt 0 ]]; do
case "$1" in
--bump) shift ;; # default on — silently accepted for backward compat
--no-bump) BUMP_IOS=false; BUMP_ANDROID=false; shift ;;
--version) EXPLICIT_VERSION="$2"; shift 2 ;;
--build) EXPLICIT_BUILD="$2"; shift 2 ;;
--android-version-code) ANDROID_VERSION_CODE_OVERRIDE="$2"; shift 2 ;;
--notes) RELEASE_NOTES="$2"; shift 2 ;;
--skip-clean) SKIP_CLEAN=true; shift ;;
--skip-validate) SKIP_VALIDATE=true; shift ;;
--skip-submit) SKIP_SUBMIT=true; shift ;;
--keep-build) KEEP_BUILD=true; shift ;;
--dry-run) DRY_RUN=true; shift ;;
-v|--verbose) VERBOSE=true; shift ;;
-h|--help)
awk '/^#!/{next} /^#/{sub(/^# ?/, ""); print; next} {exit}' "$0"
exit 0 ;;
*) die "Unbekannter Flag: $1 (--help für Hilfe)" ;;
esac
done
# ═══════════════════════════════════════════════════════════════════════════
# ENV & Paths
# ═══════════════════════════════════════════════════════════════════════════
REBREAK_TEAM_ID="${REBREAK_TEAM_ID:-84BQ7MTFYK}"
MDM_SERVER="${MDM_SERVER:-rebreak-mdm}"
INSTALL_BASE_URL="${INSTALL_BASE_URL:-https://mdm.rebreak.org/install}"
export REBREAK_ENABLE_FAMILY_CONTROLS="${REBREAK_ENABLE_FAMILY_CONTROLS:-1}"
export EXPO_PUBLIC_ENABLE_DEBUG="${EXPO_PUBLIC_ENABLE_DEBUG:-0}"
IOS_DIR="$SCRIPT_DIR/ios"
ANDROID_DIR="$SCRIPT_DIR/android"
ARCHIVE_PATH="/tmp/Rebreak.xcarchive"
ADHOC_EXPORT_DIR="/tmp/Rebreak-ipa"
TF_EXPORT_DIR="/tmp/Rebreak-tf"
ADHOC_IPA="$ADHOC_EXPORT_DIR/Rebreak.ipa"
TF_IPA="$TF_EXPORT_DIR/Rebreak.ipa"
ADHOC_EXPORT_OPTIONS="$SCRIPT_DIR/build-config/exportOptions-adhoc.plist"
TF_EXPORT_OPTIONS="$SCRIPT_DIR/build-config/exportOptions-tf.plist"
WORKSPACE="$IOS_DIR/ReBreak.xcworkspace"
SCHEME="ReBreak"
APPLE_ID_EMAIL="${APPLE_ID_EMAIL:-chahinebrini@gmail.com}"
APPLE_APP_SPECIFIC_PASSWORD="${APPLE_APP_SPECIFIC_PASSWORD:-}"
ASC_API_KEY_PATH="${ASC_API_KEY_PATH:-}"
ASC_API_KEY_ID="${ASC_API_KEY_ID:-}"
ASC_API_KEY_ISSUER="${ASC_API_KEY_ISSUER:-}"
PLAY_SERVICE_ACCOUNT_JSON="${PLAY_SERVICE_ACCOUNT_JSON:-$HOME/secrets/rebreak-play-service-account.json}"
mkdir -p "$LOG_DIR" 2>/dev/null || true
# ═══════════════════════════════════════════════════════════════════════════
# Helpers
# ═══════════════════════════════════════════════════════════════════════════
get_current_version() {
grep -E '"version"' "$PACKAGE_JSON" | head -1 \
| sed -E 's/[^"]*"version"[^"]*"([^"]+)".*/\1/' || echo "0.0.0"
}
get_current_build_number() {
grep -E 'buildNumber:' "$APP_CONFIG" \
| sed -E 's/[^:]*:[^"]*"([0-9]+)".*/\1/' || echo "0"
}
get_current_version_code() {
grep -E 'versionCode:' "$APP_CONFIG" \
| sed -E 's/[^:]*:[^0-9]*([0-9]+).*/\1/' || echo "0"
}
bump_ios_version() {
log "iOS Version Bump..."
local current_version
current_version=$(get_current_version)
local current_build
current_build=$(get_current_build_number)
local new_version="$current_version"
local new_build
if [[ -n "$EXPLICIT_VERSION" ]]; then
new_version="$EXPLICIT_VERSION"
fi
if [[ -n "$EXPLICIT_BUILD" ]]; then
new_build="$EXPLICIT_BUILD"
else
new_build=$((current_build + 1))
fi
echo " $current_version (Build $current_build) → $new_version (Build $new_build)"
if ! $DRY_RUN; then
# Update package.json version
if [[ "$new_version" != "$current_version" ]]; then
if [[ "$(uname)" == "Darwin" ]]; then
sed -i '' "s/\"version\": \"$current_version\"/\"version\": \"$new_version\"/" "$PACKAGE_JSON"
else
sed -i "s/\"version\": \"$current_version\"/\"version\": \"$new_version\"/" "$PACKAGE_JSON"
fi
fi
# Update buildNumber in app.config.ts
if [[ "$(uname)" == "Darwin" ]]; then
sed -i '' "s/buildNumber: \"$current_build\"/buildNumber: \"$new_build\"/" "$APP_CONFIG"
else
sed -i "s/buildNumber: \"$current_build\"/buildNumber: \"$new_build\"/" "$APP_CONFIG"
fi
# Update Extension Info.plists
local ext_plists=(
"$SCRIPT_DIR/modules/rebreak-protection/ios/RebreakContentFilter/Info.plist"
"$SCRIPT_DIR/modules/rebreak-protection/ios/RebreakPacketTunnelExtension/Info.plist"
"$SCRIPT_DIR/modules/rebreak-protection/ios/RebreakURLFilterExtension/Info.plist"
)
for plist in "${ext_plists[@]}"; do
if [[ -f "$plist" ]]; then
/usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString $new_version" "$plist" 2>/dev/null || true
/usr/libexec/PlistBuddy -c "Set :CFBundleVersion $new_build" "$plist" 2>/dev/null || true
fi
done
ok "iOS Version aktualisiert"
fi
}
bump_android_version() {
log "Android versionCode Bump..."
local current_version_code
current_version_code=$(get_current_version_code)
local new_version_code
if [[ -n "$ANDROID_VERSION_CODE_OVERRIDE" ]]; then
new_version_code="$ANDROID_VERSION_CODE_OVERRIDE"
else
new_version_code=$((current_version_code + 1))
fi
echo " versionCode: $current_version_code$new_version_code"
if ! $DRY_RUN; then
if [[ "$(uname)" == "Darwin" ]]; then
sed -i '' "s/versionCode: $current_version_code,/versionCode: $new_version_code,/" "$APP_CONFIG"
else
sed -i "s/versionCode: $current_version_code,/versionCode: $new_version_code,/" "$APP_CONFIG"
fi
ok "Android versionCode aktualisiert"
fi
}
# ═══════════════════════════════════════════════════════════════════════════
# Release Notes Collection
# ═══════════════════════════════════════════════════════════════════════════
collect_release_notes() {
# Priority: --notes flag > NEXT_RELEASE.md > interactive prompt
local next_release_file="$SCRIPT_DIR/NEXT_RELEASE.md"
if [[ -n "$RELEASE_NOTES" ]]; then
log "Release-Notes: aus --notes Flag"
return
fi
if [[ -f "$next_release_file" ]] && [[ -s "$next_release_file" ]]; then
RELEASE_NOTES="$(cat "$next_release_file")"
log "Release-Notes: aus $next_release_file"
return
fi
# Interactive prompt (nur wenn TTY)
if [[ -t 0 ]] && [[ -t 1 ]] && ! $DRY_RUN; then
echo ""
echo "${BOLD}Release-Notes für diese Version (optional):${RESET}"
echo "${YELLOW}Tipp: Multi-line mit Strg-D beenden, oder ENTER für skip${RESET}"
echo ""
# Read multi-line input until EOF (Ctrl-D) or empty line
local input_lines=()
local line
while IFS= read -r line; do
if [[ -z "$line" ]] && [[ ${#input_lines[@]} -eq 0 ]]; then
# First line empty = skip
break
fi
input_lines+=("$line")
done
if [[ ${#input_lines[@]} -gt 0 ]]; then
RELEASE_NOTES="$(printf '%s\n' "${input_lines[@]}")"
log "Release-Notes: interaktiv erfasst"
fi
fi
}
archive_release_notes_to_changelog() {
[[ -z "$RELEASE_NOTES" ]] && return
local changelog="$SCRIPT_DIR/CHANGELOG.md"
local version build version_code
version="$(get_current_version)"
build="$(get_current_build_number)"
version_code="$(get_current_version_code)"
local date_stamp
date_stamp="$(date +%Y-%m-%d)"
local header="## v${version} (Build ${build} / versionCode ${version_code}) — ${date_stamp}"
local entry="${header}\n\n${RELEASE_NOTES}\n"
if [[ ! -f "$changelog" ]]; then
# Create new CHANGELOG.md with header
echo "# Changelog" > "$changelog"
echo "" >> "$changelog"
echo "Alle wichtigen Änderungen an diesem Projekt werden in dieser Datei dokumentiert." >> "$changelog"
echo "" >> "$changelog"
fi
# Prepend new entry after header (assumes "# Changelog" on line 1)
if $DRY_RUN; then
log "[DRY-RUN] Würde Release-Notes in $changelog archivieren"
else
# Use temporary file to prepend
local temp_file
temp_file="$(mktemp)"
{
head -3 "$changelog" 2>/dev/null || echo -e "# Changelog\n"
echo "$entry"
tail -n +4 "$changelog" 2>/dev/null || true
} > "$temp_file"
mv "$temp_file" "$changelog"
ok "Release-Notes in $changelog archiviert"
fi
# Clear NEXT_RELEASE.md if it was used
local next_release_file="$SCRIPT_DIR/NEXT_RELEASE.md"
if [[ -f "$next_release_file" ]] && ! $DRY_RUN; then
rm "$next_release_file"
ok "NEXT_RELEASE.md geleert"
fi
}
# ═══════════════════════════════════════════════════════════════════════════
# iOS Ad-Hoc / MDM Pipeline
# ═══════════════════════════════════════════════════════════════════════════
deploy_mdm() {
section "iOS Ad-Hoc (MDM)"
# Preflight
command -v xcodebuild >/dev/null 2>&1 || die "xcodebuild nicht gefunden"
command -v ssh >/dev/null 2>&1 || die "ssh nicht gefunden"
command -v scp >/dev/null 2>&1 || die "scp nicht gefunden"
[[ -f "$ADHOC_EXPORT_OPTIONS" ]] || die "ExportOptions nicht gefunden: $ADHOC_EXPORT_OPTIONS"
[[ -d "$IOS_DIR" ]] || die "ios/ nicht gefunden — expo prebuild zuerst ausführen"
log "Prüfe SSH-Verbindung zu $MDM_SERVER..."
if ! ssh -o ConnectTimeout=10 -o BatchMode=yes "$MDM_SERVER" "echo ok" >/dev/null 2>&1; then
die "SSH zu $MDM_SERVER fehlgeschlagen — VPN oder SSH-Key prüfen"
fi
ok "SSH OK"
# Clean
if ! $SKIP_CLEAN; then
log "Clean iOS..."
run "$SCRIPT_DIR/clean-ios.sh"
fi
# Archive
rm -rf "$ARCHIVE_PATH"
run_quiet "Building xcarchive" "$LOG_DIR/mdm-archive-$TIMESTAMP.log" \
xcodebuild archive \
-workspace "$WORKSPACE" \
-scheme "$SCHEME" \
-configuration Release \
-archivePath "$ARCHIVE_PATH" \
-destination 'generic/platform=iOS' \
DEVELOPMENT_TEAM="$REBREAK_TEAM_ID"
ok "xcarchive fertig: $ARCHIVE_PATH"
# Export IPA
rm -rf "$ADHOC_EXPORT_DIR"
run_quiet "Exporting Ad-Hoc IPA" "$LOG_DIR/mdm-export-$TIMESTAMP.log" \
xcodebuild -exportArchive \
-archivePath "$ARCHIVE_PATH" \
-exportPath "$ADHOC_EXPORT_DIR" \
-exportOptionsPlist "$ADHOC_EXPORT_OPTIONS"
[[ -f "$ADHOC_IPA" ]] || die "IPA nicht erzeugt: $ADHOC_IPA"
ok "IPA exportiert: $ADHOC_IPA"
# Upload
log "Uploading zu $MDM_SERVER..."
run scp "$ADHOC_IPA" "$MDM_SERVER:/opt/nanomdm/install/Rebreak.ipa"
run scp "$ADHOC_EXPORT_DIR/manifest.plist" "$MDM_SERVER:/opt/nanomdm/install/manifest.plist"
ok "MDM-Deploy abgeschlossen"
echo ""
echo " Install-URL: $INSTALL_BASE_URL/manifest.plist"
echo " Server-seitiger systemd path-watcher triggert MDM-Push automatisch"
}
# ═══════════════════════════════════════════════════════════════════════════
# iOS TestFlight Pipeline
# ═══════════════════════════════════════════════════════════════════════════
deploy_testflight() {
section "iOS TestFlight"
# Preflight
command -v xcodebuild >/dev/null 2>&1 || die "xcodebuild nicht gefunden"
command -v xcrun >/dev/null 2>&1 || die "xcrun nicht gefunden"
[[ -f "$TF_EXPORT_OPTIONS" ]] || die "ExportOptions nicht gefunden: $TF_EXPORT_OPTIONS"
# Auth
local AUTH_MODE=""
if [[ -n "$ASC_API_KEY_PATH" && -n "$ASC_API_KEY_ID" && -n "$ASC_API_KEY_ISSUER" ]]; then
AUTH_MODE="api-key"
[[ -f "$ASC_API_KEY_PATH" ]] || die "ASC API-Key nicht gefunden: $ASC_API_KEY_PATH"
log "Auth: ASC API-Key ($ASC_API_KEY_ID)"
elif [[ -n "$APPLE_APP_SPECIFIC_PASSWORD" ]]; then
AUTH_MODE="app-specific-password"
log "Auth: App-Specific-Password ($APPLE_ID_EMAIL)"
else
die "Kein Auth konfiguriert.
Benötigt einen der folgenden Auth-Wege:
Option A — App-Specific-Password:
export APPLE_ID_EMAIL=chahinebrini@gmail.com
export APPLE_APP_SPECIFIC_PASSWORD=xxxx-xxxx-xxxx-xxxx
Passwort generieren: https://appleid.apple.com → Sicherheit
Option B — ASC API-Key (besser für CI):
export ASC_API_KEY_PATH=/pfad/zu/AuthKey_ABCDE12345.p8
export ASC_API_KEY_ID=ABCDE12345
export ASC_API_KEY_ISSUER=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
Key erstellen: https://appstoreconnect.apple.com → Users → Integrations"
fi
# Archive lokalisieren
local USED_ARCHIVE="$ARCHIVE_PATH"
if [[ ! -d "$USED_ARCHIVE" ]]; then
# Fallback: neuestes Xcode-Archive
USED_ARCHIVE=$(find ~/Library/Developer/Xcode/Archives -name "ReBreak*.xcarchive" -type d 2>/dev/null \
| sort -r | head -1 || true)
if [[ -z "$USED_ARCHIVE" ]]; then
die "Kein xcarchive gefunden.
Entweder:
1. ./deploy.sh mdm zuerst ausführen (erzeugt $ARCHIVE_PATH)
2. Oder: ./deploy.sh all (baut MDM + TF in einem Lauf)"
fi
log "Auto-detect: $USED_ARCHIVE"
else
log "Verwende Archive: $USED_ARCHIVE"
fi
# Export IPA
rm -rf "$TF_EXPORT_DIR"
run_quiet "Exporting App-Store IPA" "$LOG_DIR/tf-export-$TIMESTAMP.log" \
xcodebuild -exportArchive \
-archivePath "$USED_ARCHIVE" \
-exportPath "$TF_EXPORT_DIR" \
-exportOptionsPlist "$TF_EXPORT_OPTIONS"
[[ -f "$TF_IPA" ]] || die "IPA nicht erzeugt: $TF_IPA"
ok "IPA exportiert: $TF_IPA"
# Validate
if ! $SKIP_VALIDATE; then
if [[ "$AUTH_MODE" == "api-key" ]]; then
run_quiet "Validating IPA (App-Store Connect)" "$LOG_DIR/tf-validate-$TIMESTAMP.log" \
xcrun altool --validate-app \
-f "$TF_IPA" \
-t ios \
--apiKey "$ASC_API_KEY_ID" \
--apiIssuer "$ASC_API_KEY_ISSUER"
else
run_quiet "Validating IPA (App-Store Connect)" "$LOG_DIR/tf-validate-$TIMESTAMP.log" \
xcrun altool --validate-app \
-f "$TF_IPA" \
-t ios \
-u "$APPLE_ID_EMAIL" \
-p "$APPLE_APP_SPECIFIC_PASSWORD"
fi
fi
# Upload
if [[ "$AUTH_MODE" == "api-key" ]]; then
run_quiet "Uploading zu App-Store Connect (TestFlight)" "$LOG_DIR/tf-upload-$TIMESTAMP.log" \
xcrun altool --upload-app \
-f "$TF_IPA" \
-t ios \
--apiKey "$ASC_API_KEY_ID" \
--apiIssuer "$ASC_API_KEY_ISSUER"
else
run_quiet "Uploading zu App-Store Connect (TestFlight)" "$LOG_DIR/tf-upload-$TIMESTAMP.log" \
xcrun altool --upload-app \
-f "$TF_IPA" \
-t ios \
-u "$APPLE_ID_EMAIL" \
-p "$APPLE_APP_SPECIFIC_PASSWORD"
fi
ok "TestFlight-Deploy abgeschlossen"
echo ""
echo " IPA erscheint automatisch in Internal Testing"
echo " Status: https://appstoreconnect.apple.com"
}
# ═══════════════════════════════════════════════════════════════════════════
# Android Pipeline
# ═══════════════════════════════════════════════════════════════════════════
deploy_android() {
section "Android Release"
# Preflight
[[ -d "$ANDROID_DIR" ]] || die "android/ nicht gefunden — expo prebuild zuerst ausführen"
local KEYSTORE_PROPS="$ANDROID_DIR/key.properties"
if [[ ! -f "$KEYSTORE_PROPS" ]]; then
error "Android Signing nicht konfiguriert"
echo ""
echo "Fehlt: $KEYSTORE_PROPS"
echo ""
echo "Setup-Schritte:"
echo ""
echo "1. Keystore generieren:"
echo " keytool -genkey -v -keystore ~/rebreak-release.keystore \\"
echo " -alias rebreak -keyalg RSA -keysize 2048 -validity 10000"
echo ""
echo "2. Keystore nach android/app/ kopieren:"
echo " cp ~/rebreak-release.keystore $ANDROID_DIR/app/"
echo ""
echo "3. key.properties erstellen:"
echo " cat > $KEYSTORE_PROPS << EOF"
echo "storePassword=<dein-password>"
echo "keyPassword=<dein-password>"
echo "keyAlias=rebreak"
echo "storeFile=rebreak-release.keystore"
echo "EOF"
echo ""
echo "4. NIEMALS committen — .gitignore prüfen"
exit 1
fi
log "Keystore-Config gefunden: $KEYSTORE_PROPS"
# Android SDK: ANDROID_HOME env oder Standard-macOS-Pfad. Auch local.properties
# automatisch erzeugen, damit gradle ohne env-export funktioniert.
if [[ -z "${ANDROID_HOME:-}" ]]; then
if [[ -d "$HOME/Library/Android/sdk" ]]; then
export ANDROID_HOME="$HOME/Library/Android/sdk"
log "ANDROID_HOME auto-detected: $ANDROID_HOME"
else
die "ANDROID_HOME nicht gesetzt und SDK nicht in ~/Library/Android/sdk gefunden — Android Studio installieren oder ANDROID_HOME setzen"
fi
fi
if [[ ! -f "$ANDROID_DIR/local.properties" ]]; then
echo "sdk.dir=$ANDROID_HOME" > "$ANDROID_DIR/local.properties"
log "android/local.properties erzeugt"
fi
# Build
run_quiet "Building Release AAB (gradlew bundleRelease)" \
"$LOG_DIR/android-build-$TIMESTAMP.log" \
bash -c "cd $ANDROID_DIR && ANDROID_HOME='$ANDROID_HOME' ./gradlew bundleRelease --console=plain"
local AAB="$ANDROID_DIR/app/build/outputs/bundle/release/app-release.aab"
[[ -f "$AAB" ]] || die "AAB nicht erzeugt: $AAB"
ok "AAB gebaut: $AAB"
# Submit
if ! $SKIP_SUBMIT; then
if [[ ! -f "$PLAY_SERVICE_ACCOUNT_JSON" ]]; then
warn "Play Console Service-Account-JSON fehlt: $PLAY_SERVICE_ACCOUNT_JSON"
echo ""
echo "Setup-Schritte:"
echo "1. Google Cloud Console → Service Accounts → Create → JSON-Key"
echo "2. Play Console → Setup → API-Access → Service-Account linken"
echo "3. Permissions: 'Releases' (Edit + Read)"
echo "4. JSON-Key ablegen:"
echo " mkdir -p ~/secrets"
echo " mv ~/Downloads/rebreak-play-*.json ~/secrets/rebreak-play-service-account.json"
echo ""
echo "Oder ENV setzen:"
echo " export PLAY_SERVICE_ACCOUNT_JSON=/pfad/zu/key.json"
echo ""
echo "Skipped Submit — AAB ist gebaut und bereit für manuellen Upload"
else
log "Submitting zu Play Console Internal Track..."
local eas_bin
eas_bin="$(command -v eas || true)"
if [[ -z "$eas_bin" ]]; then
die "eas-cli nicht gefunden. Installiere mit: pnpm add -g eas-cli (oder npm i -g eas-cli)"
fi
run "$eas_bin" submit --platform android \
--path "$AAB" \
--profile production \
--non-interactive
ok "Play Console Submit abgeschlossen"
fi
else
log "Submit skipped (--skip-submit)"
fi
ok "Android-Deploy abgeschlossen"
echo ""
echo " AAB: $AAB"
if ! $SKIP_SUBMIT && [[ -f "$PLAY_SERVICE_ACCOUNT_JSON" ]]; then
echo " Status: https://play.google.com/console"
fi
}
# ═══════════════════════════════════════════════════════════════════════════# Cleanup nach erfolgreichem Submit
# ═════════════════════════════════════════════════════════════════════════
human_size() {
# Cross-platform du -sh fallback for missing path
local p="$1"
[[ -e "$p" ]] || { echo "-"; return; }
du -sh "$p" 2>/dev/null | awk '{print $1}'
}
cleanup_build_artifacts() {
if $KEEP_BUILD; then
log "Cleanup skipped (--keep-build)"
return
fi
if $DRY_RUN; then
log "Cleanup (dry-run) — würde löschen:"
else
log "Cleanup Build-Artefakte..."
fi
local freed_paths=()
if $DO_MDM || $DO_TF; then
for p in "$ARCHIVE_PATH" "$ADHOC_EXPORT_DIR" "$TF_EXPORT_DIR"; do
if [[ -e "$p" ]]; then
echo " $(human_size "$p")\t$p"
freed_paths+=("$p")
fi
done
fi
if $DO_ANDROID; then
for p in "$ANDROID_DIR/app/build" "$ANDROID_DIR/build" "$ANDROID_DIR/.gradle"; do
if [[ -e "$p" ]]; then
echo " $(human_size "$p")\t$p"
freed_paths+=("$p")
fi
done
fi
if ! $DRY_RUN; then
for p in "${freed_paths[@]}"; do
rm -rf "$p" 2>/dev/null || true
done
fi
ok "Cleanup fertig (${#freed_paths[@]} Pfade)"
}
# ═════════════════════════════════════════════════════════════════════════# Main
# ═══════════════════════════════════════════════════════════════════════════
echo ""
log "ReBreak Multi-Platform Deploy"
echo ""
echo "Targets:"
if $DO_MDM; then echo " ${GREEN}${RESET} iOS Ad-Hoc/MDM"; fi
if $DO_TF; then echo " ${GREEN}${RESET} iOS TestFlight"; fi
if $DO_ANDROID; then echo " ${GREEN}${RESET} Android"; fi
echo ""
# Collect Release Notes early (before bumping)
collect_release_notes
# Version Bumping
if $BUMP_IOS && ($DO_MDM || $DO_TF); then
bump_ios_version
fi
if $BUMP_ANDROID && $DO_ANDROID; then
bump_android_version
fi
# Deploy
if $DO_MDM; then
deploy_mdm
fi
if $DO_TF; then
deploy_testflight
fi
if $DO_ANDROID; then
deploy_android
fi
# Cleanup (default on — spart Mac-Speicher; --keep-build zum Opt-out)
cleanup_build_artifacts
# Archive Release Notes
archive_release_notes_to_changelog
# Summary
echo ""
section "✓ Deploy Abgeschlossen"
echo ""
echo "Logs: $LOG_DIR"
echo ""
if [[ -n "$RELEASE_NOTES" ]]; then
echo "${BOLD}═══════════════════════════════════════════════════════════${RESET}"
echo "${BOLD}Release-Notes für v$(get_current_version) (Build $(get_current_build_number)):${RESET}"
echo "${BOLD}═══════════════════════════════════════════════════════════${RESET}"
echo ""
echo "$RELEASE_NOTES"
echo ""
echo "${BOLD}═══════════════════════════════════════════════════════════${RESET}"
echo ""
echo "${YELLOW}→ Copy-Paste in:${RESET}"
if $DO_TF; then
echo " ${BLUE}${RESET} TestFlight: https://appstoreconnect.apple.com → TestFlight → Internal Testing → 'What to Test'"
fi
if $DO_ANDROID; then
echo " ${BLUE}${RESET} Play Console: https://play.google.com/console → Internal Testing → Release-Notes"
fi
echo ""
fi
if ! $DRY_RUN; then
echo "Nächste Schritte:"
echo " - Änderungen committen (Version-Bump + CHANGELOG.md)"
echo " - Git-Tag erstellen: git tag -a v$(get_current_version) -m 'Release $(get_current_version)'"
echo " - Push: git push && git push --tags"
fi