961 lines
36 KiB
Bash
Executable File
961 lines
36 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
|
|
|
|
# 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 "$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
|
|
# 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=<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
|