chahinebrini 061bd2d799 fix(deploy): allow flags before subcommand (./deploy.sh --skip-pods)
Treat $1 as subcommand only if it doesn't start with '-', else default to 'all'.
2026-05-30 10:03:11 +02:00

927 lines
34 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-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
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 "$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
}
# run_quiet "Label" <log-file> <cmd...>
# 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
if $VERBOSE || [[ ! -t 1 ]]; then
log "$label"
"$@" 2>&1 | tee "$logfile"
return ${PIPESTATUS[0]}
fi
local start=$SECONDS
local expected pid elapsed subtitle
expected=$(runtime_lookup "$label" || echo 0)
expected=${expected:-0}
RUN_QUIET_I=0
( "$@" >"$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"
local rc=$?
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=<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