#!/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-pods nur prebuild + pod install überspringen (clean-ios.sh läuft sonst) # --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: # Persistenz (empfohlen): siehe .env.deploy.local.example # cp .env.deploy.local.example .env.deploy.local # gitignored # # einmalig editieren — deploy.sh source'd das automatisch # # iOS TestFlight / Ad-Hoc: # - ASC_API_KEY_PATH + ASC_API_KEY_ID + ASC_API_KEY_ISSUER (Pflicht) # 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 # ERR-Trap: zeigt die exakte Zeile + Command der set -e ausgelöst hat trap 'rc=$?; set +u; echo "" >&2; echo "✗ deploy.sh aborted (rc=$rc)" >&2; echo " line $LINENO: $BASH_COMMAND" >&2; if [[ -n "${FUNCNAME+x}" && ${#FUNCNAME[@]} -gt 0 ]]; then echo " call stack:" >&2; for ((i=0;i<${#FUNCNAME[@]};i++)); do echo " #$i ${FUNCNAME[$i]} (${BASH_SOURCE[$i]}:${BASH_LINENO[$i]})" >&2; done; fi; set -u' ERR # Ctrl+C / SIGTERM: kill background children (xcodebuild etc.) cleanly cleanup_children() { local jobs_pids jobs_pids=$(jobs -p 2>/dev/null || true) if [[ -n "$jobs_pids" ]]; then echo "" >&2 echo "⚠ Abbruch — beende laufende Build-Prozesse..." >&2 # shellcheck disable=SC2086 kill $jobs_pids 2>/dev/null || true sleep 0.5 # shellcheck disable=SC2086 kill -9 $jobs_pids 2>/dev/null || true fi exit 130 } trap cleanup_children INT TERM 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 } # Runtime-Cache für Progress-Bar (lernt Dauer pro Step über Runs hinweg) RUNTIME_CACHE="$SCRIPT_DIR/tmp/.deploy-runtimes" mkdir -p "$(dirname "$RUNTIME_CACHE")" runtime_lookup() { local label="$1" [[ -f "$RUNTIME_CACHE" ]] || return 1 grep -aE "^$(printf '%s' "$label" | sed 's/[][\.*^$/]/\\&/g')\|" "$RUNTIME_CACHE" 2>/dev/null \ | tail -1 | cut -d'|' -f2 } runtime_save() { local label="$1" duration="$2" # Keep only last entry per label if [[ -f "$RUNTIME_CACHE" ]]; then grep -avE "^$(printf '%s' "$label" | sed 's/[][\.*^$/]/\\&/g')\|" "$RUNTIME_CACHE" > "$RUNTIME_CACHE.tmp" || true mv "$RUNTIME_CACHE.tmp" "$RUNTIME_CACHE" fi echo "$label|$duration" >> "$RUNTIME_CACHE" } # Render brew-style SINGLE-LINE progress bar: # ==> Building xcarchive ████████░░░░░░░░ 42% (1m23s/~3m18s) ↳ CompileSwift Foo render_progress() { local elapsed="$1" expected="$2" label="$3" subtitle="$4" local width=20 pct filled empty bar elapsed_h expected_h line if (( expected > 0 )); then pct=$(( elapsed * 100 / expected )) (( pct > 99 )) && pct=99 filled=$(( elapsed * width / expected )) (( filled > width )) && filled=$width else # No baseline — animated indeterminate bar position pct=0 filled=$(( RUN_QUIET_I % (width * 2) )) (( filled > width )) && filled=$(( width * 2 - filled )) fi empty=$(( width - filled )) bar=$(printf '%*s' "$filled" '' | tr ' ' '█')$(printf '%*s' "$empty" '' | tr ' ' '░') elapsed_h=$(format_duration "$elapsed") if (( expected > 0 )); then expected_h=$(format_duration_rounded "$expected") line=$(printf '%s==>%s %s %s %s%3d%%%s (%s/~%s)' \ "$BLUE" "$RESET" "$label" "$bar" "$YELLOW" "$pct" "$RESET" "$elapsed_h" "$expected_h") else line=$(printf '%s==>%s %s %s %s(%s)%s' \ "$BLUE" "$RESET" "$label" "$bar" "$YELLOW" "$elapsed_h" "$RESET") fi if [[ -n "$subtitle" ]]; then line="$line ↳ $subtitle" fi # Truncate to terminal width to avoid line-wrap garbling the redraw local cols=${COLUMNS:-$(tput cols 2>/dev/null || echo 120)} printf '\r\033[K%s' "${line:0:cols}" >&2 } format_duration() { local s="$1" if (( s < 60 )); then printf '%ds' "$s" else printf '%dm%02ds' $((s / 60)) $((s % 60)) fi } # Rounded format for ETA display: nearest minute when >= 90s, half-minute when 60-89s format_duration_rounded() { local s="$1" if (( s < 60 )); then printf '%ds' "$s" elif (( s < 90 )); then printf '1m' else # Round to nearest minute (>=30s rounds up) local m=$(( (s + 30) / 60 )) printf '%dm' "$m" fi } # run_quiet "Label" # Runs cmd silently with a brew-style progress bar (time-based, learns durations # across runs). 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 # Debug / verbose mode: stream output directly via tee (no subshell, no spinner) if $VERBOSE || [[ "${RUN_QUIET_DEBUG:-0}" = "1" ]] || [[ ! -t 1 ]]; then log "$label" set +e "$@" 2>&1 | tee "$logfile" local prc=${PIPESTATUS[0]} set -e if [[ $prc -eq 0 ]]; then ok "$label" else error "$label fehlgeschlagen (exit $prc) — voller Log: $logfile" exit $prc fi return 0 fi local start=$SECONDS local expected pid elapsed subtitle rc expected=$(runtime_lookup "$label" || echo 0) expected=${expected:-0} RUN_QUIET_I=0 # Disable set -e/pipefail around backgrounding so wait can capture rc cleanly # (bash 3.2 on macOS aborts unexpectedly with the subshell+wait pattern under set -e) set +e "$@" >"$logfile" 2>&1 & pid=$! while kill -0 "$pid" 2>/dev/null; do elapsed=$((SECONDS - start)) RUN_QUIET_I=$((RUN_QUIET_I + 1)) subtitle="" if [[ -f "$logfile" ]]; then # Primary: meaningful build action (filtered) subtitle=$(tail -20 "$logfile" 2>/dev/null \ | grep -aE '^(Compiling|CompileSwift|CompileC|Linking|Ld|Touch|CodeSign|ProcessProductPackaging|ExtractAppIntentsMetadata|Validate|Archive|GenerateAssetSymbols|CopySwiftLibs|PhaseScriptExecution|> Task|BUILD|\[CP|\[Pods)' \ | tail -1 \ | sed -E 's|.*/||; s|\(.*||' \ | cut -c1-50) # Fallback: any last non-empty line (so user sees activity during setup/parsing) if [[ -z "$subtitle" ]]; then subtitle=$(tail -5 "$logfile" 2>/dev/null \ | grep -av '^[[:space:]]*$' \ | tail -1 \ | sed -E 's|^[[:space:]]+||' \ | cut -c1-50) fi fi render_progress "$elapsed" "$expected" "$label" "$subtitle" sleep 0.2 done wait "$pid" rc=$? set -e elapsed=$((SECONDS - start)) # Clear progress line printf '\r\033[K' >&2 if [[ $rc -eq 0 ]]; then ok "$label ${YELLOW}($(format_duration "$elapsed"))${RESET}" runtime_save "$label" "$elapsed" else error "$label fehlgeschlagen nach $(format_duration "$elapsed") (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="all" if [[ $# -gt 0 && "$1" != -* ]]; then COMMAND="$1" shift fi 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_PODS=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-pods) SKIP_PODS=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 # ═══════════════════════════════════════════════════════════════════════════ # Secrets-File auto-loading (NICHT committen — siehe .env.deploy.local.example) # ═══════════════════════════════════════════════════════════════════════════ # Lädt automatisch: # apps/rebreak-native/.env.deploy.local (lokal, gitignored) # ~/.config/rebreak/deploy.env (global fallback, optional) for secrets_file in "$SCRIPT_DIR/.env.deploy.local" "$HOME/.config/rebreak/deploy.env"; do if [[ -f "$secrets_file" ]]; then # shellcheck disable=SC1090 set -a; source "$secrets_file"; set +a log "Secrets geladen aus: $secrets_file" break fi 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}" ASC_API_KEY_PATH="${ASC_API_KEY_PATH:-}" ASC_API_KEY_ID="${ASC_API_KEY_ID:-}" ASC_API_KEY_ISSUER="${ASC_API_KEY_ISSUER:-}" # Build xcodebuild auth-args (ASC API-Key enables automatic cert/profile download) xcodebuild_auth_args() { if [[ -n "$ASC_API_KEY_PATH" && -n "$ASC_API_KEY_ID" && -n "$ASC_API_KEY_ISSUER" ]]; then echo "-allowProvisioningUpdates -authenticationKeyPath $ASC_API_KEY_PATH -authenticationKeyID $ASC_API_KEY_ID -authenticationKeyIssuerID $ASC_API_KEY_ISSUER" fi } # Preflight check for ASC API-Key — fails fast with clear message before xcodebuild starts require_asc_api_key() { local missing=() [[ -n "$ASC_API_KEY_ID" ]] || missing+=("ASC_API_KEY_ID") [[ -n "$ASC_API_KEY_ISSUER" ]] || missing+=("ASC_API_KEY_ISSUER") [[ -n "$ASC_API_KEY_PATH" ]] || missing+=("ASC_API_KEY_PATH") if (( ${#missing[@]} > 0 )); then die "iOS Signing braucht ASC API-Key. Fehlt: ${missing[*]} → Editiere apps/rebreak-native/.env.deploy.local (siehe .env.deploy.local.example)" fi if [[ ! -f "$ASC_API_KEY_PATH" ]]; then die "ASC API-Key Datei existiert nicht: $ASC_API_KEY_PATH → Lade .p8 von https://appstoreconnect.apple.com/access/integrations/api → Lege ab unter: $ASC_API_KEY_PATH" fi } 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" require_asc_api_key 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..." local clean_args=(--quiet) $SKIP_PODS && clean_args+=(--skip-pods) run "$SCRIPT_DIR/clean-ios.sh" "${clean_args[@]}" fi # Archive rm -rf "$ARCHIVE_PATH" # shellcheck disable=SC2046 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" \ $(xcodebuild_auth_args) ok "xcarchive fertig: $ARCHIVE_PATH" # Export IPA rm -rf "$ADHOC_EXPORT_DIR" # shellcheck disable=SC2046 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" \ $(xcodebuild_auth_args) [[ -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" require_asc_api_key # Auth — require_asc_api_key bereits im Preflight oben gelaufen log "Auth: ASC API-Key ($ASC_API_KEY_ID)" # 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" # shellcheck disable=SC2046 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" \ $(xcodebuild_auth_args) [[ -f "$TF_IPA" ]] || die "IPA nicht erzeugt: $TF_IPA" ok "IPA exportiert: $TF_IPA" # Validate if ! $SKIP_VALIDATE; 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" fi # Upload 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" 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=" echo "keyPassword=" 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