#!/bin/bash # Script de déploiement iOS automatisé pour GEOSECTOR # Version: 1.0 # Date: 2025-12-05 # Auteur: Pierre (avec l'aide de Claude) # # Usage: # ./deploy-ios-full-auto.sh # Utilise ../VERSION # ./deploy-ios-full-auto.sh 3.6.0 # Version spécifique # ./deploy-ios-full-auto.sh 3.6.0 --skip-build # Skip Flutter build si déjà fait set -euo pipefail # ===================================== # Configuration # ===================================== # Couleurs pour les messages RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' MAGENTA='\033[0;35m' CYAN='\033[0;36m' NC='\033[0m' # No Color # Configuration Mac mini MAC_MINI_HOST="minipi4" # Nom défini dans ~/.ssh/config MAC_BASE_DIR="/Users/pierre/dev/geosector" # Timestamp pour logs et archives TIMESTAMP=$(date +%Y%m%d_%H%M%S) LOG_FILE="./logs/deploy-ios-${TIMESTAMP}.log" mkdir -p ./logs # Variables globales pour le rapport STEP_START_TIME=0 TOTAL_START_TIME=$(date +%s) ERRORS_COUNT=0 WARNINGS_COUNT=0 # ===================================== # Fonctions utilitaires # ===================================== log() { echo -e "$1" | tee -a "${LOG_FILE}" } log_step() { STEP_START_TIME=$(date +%s) log "\n${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" log "${CYAN}▶ $1${NC}" log "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" } log_substep() { log "${MAGENTA} ➜ $1${NC}" } log_info() { log "${BLUE}ℹ ${NC}$1" } log_success() { local elapsed=$(($(date +%s) - STEP_START_TIME)) log "${GREEN}✓${NC} $1 ${CYAN}(${elapsed}s)${NC}" } log_warning() { ((WARNINGS_COUNT++)) log "${YELLOW}⚠ ${NC}$1" } log_error() { ((ERRORS_COUNT++)) log "${RED}✗${NC} $1" } log_fatal() { log_error "$1" log "\n${RED}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" log "${RED}DÉPLOIEMENT ÉCHOUÉ${NC}" log "${RED}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" log_error "Consultez le log: ${LOG_FILE}" exit 1 } # Fonction pour exécuter une commande et capturer les erreurs safe_exec() { local cmd="$1" local error_msg="$2" if ! eval "$cmd" >> "${LOG_FILE}" 2>&1; then log_fatal "$error_msg" fi } # Fonction pour exécuter une commande SSH avec gestion d'erreurs ssh_exec() { local cmd="$1" local error_msg="$2" if ! ssh "$MAC_MINI_HOST" "$cmd" >> "${LOG_FILE}" 2>&1; then log_fatal "$error_msg" fi } # ===================================== # En-tête # ===================================== clear log "${BLUE}╔════════════════════════════════════════════════════════╗${NC}" log "${BLUE}║ ║${NC}" log "${BLUE}║ ${GREEN}🍎 DÉPLOIEMENT iOS AUTOMATISÉ${BLUE} ║${NC}" log "${BLUE}║ ${CYAN}GEOSECTOR - Full Automation${BLUE} ║${NC}" log "${BLUE}║ ║${NC}" log "${BLUE}╚════════════════════════════════════════════════════════╝${NC}" log "" log_info "Démarrage: $(date '+%Y-%m-%d %H:%M:%S')" log_info "Log file: ${LOG_FILE}" log "" # ===================================== # Étape 1 : Gestion de la version # ===================================== log_step "ÉTAPE 1/8 : Gestion de la version" # Déterminer la version à utiliser if [ "${1:-}" != "" ] && [[ ! "${1}" =~ ^-- ]]; then VERSION="$1" log_info "Version fournie en argument: ${VERSION}" else # Lire depuis ../VERSION if [ ! -f ../VERSION ]; then log_fatal "Fichier ../VERSION introuvable et aucune version fournie" fi VERSION=$(cat ../VERSION | tr -d '\n\r ' | tr -d '[:space:]') log_info "Version lue depuis ../VERSION: ${VERSION}" fi # Vérifier le format de version if ! [[ $VERSION =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then log_fatal "Format de version invalide: ${VERSION} (attendu: x.x.x)" fi # Calculer le build number BUILD_NUMBER=$(echo $VERSION | tr -d '.') FULL_VERSION="${VERSION}+${BUILD_NUMBER}" log_success "Version configurée" log_info " Version name: ${GREEN}${VERSION}${NC}" log_info " Build number: ${GREEN}${BUILD_NUMBER}${NC}" log_info " Full version: ${GREEN}${FULL_VERSION}${NC}" # ===================================== # Étape 2 : Mise à jour pubspec.yaml # ===================================== log_step "ÉTAPE 2/8 : Mise à jour pubspec.yaml" # Backup du pubspec.yaml cp pubspec.yaml pubspec.yaml.backup # Mise à jour de la version sed -i "s/^version: .*/version: $FULL_VERSION/" pubspec.yaml # Vérifier la mise à jour UPDATED_VERSION=$(grep "^version:" pubspec.yaml | sed 's/version: //' | tr -d ' ') if [ "$UPDATED_VERSION" != "$FULL_VERSION" ]; then log_fatal "Échec de la mise à jour de pubspec.yaml (attendu: $FULL_VERSION, obtenu: $UPDATED_VERSION)" fi log_success "pubspec.yaml mis à jour" # ===================================== # Étape 3 : Préparation du projet # ===================================== SKIP_BUILD=false if [[ "${2:-}" == "--skip-build" ]]; then SKIP_BUILD=true log_warning "Mode --skip-build activé, Flutter build sera ignoré" fi if [ "$SKIP_BUILD" = false ]; then log_step "ÉTAPE 3/8 : Préparation du projet Flutter" log_substep "Configuration du cache local" export PUB_CACHE="$PWD/.pub-cache-local" export GRADLE_USER_HOME="$PWD/.gradle-local" mkdir -p "$PUB_CACHE" "$GRADLE_USER_HOME" log_info " Cache Pub: $PUB_CACHE" log_info " Cache Gradle: $GRADLE_USER_HOME" log_substep "Nettoyage du projet" safe_exec "flutter clean" "Échec du nettoyage Flutter" log_substep "Récupération des dépendances" safe_exec "flutter pub get" "Échec de flutter pub get" log_substep "Application du patch nfc_manager" safe_exec "./fastlane/scripts/commun/fix-nfc-manager.sh" "Échec du patch nfc_manager" log_substep "Application du patch permission_handler (si nécessaire)" if [ -f "./fastlane/scripts/commun/fix-permission-handler.sh" ]; then safe_exec "./fastlane/scripts/commun/fix-permission-handler.sh" "Échec du patch permission_handler" fi log_substep "Génération des fichiers Hive" safe_exec "dart run build_runner build --delete-conflicting-outputs" "Échec de la génération de code" log_success "Projet préparé (dépendances + patchs + génération de code)" log_info " ⚠️ Build iOS sera fait sur le Mac mini via Fastlane" else log_step "ÉTAPE 3/8 : Préparation du projet (BUILD SKIPPED)" log_substep "Configuration du cache local uniquement" export PUB_CACHE="$PWD/.pub-cache-local" export GRADLE_USER_HOME="$PWD/.gradle-local" if [ ! -d "$PUB_CACHE" ]; then log_warning "Cache local introuvable, le build pourrait échouer sur le Mac mini" fi log_success "Cache configuré (build Flutter ignoré)" fi # ===================================== # Étape 4 : Vérification de la connexion Mac mini # ===================================== log_step "ÉTAPE 4/8 : Connexion au Mac mini" log_substep "Test de connexion SSH à ${MAC_MINI_HOST}" if ! ssh "$MAC_MINI_HOST" "echo 'Connection OK'" >> "${LOG_FILE}" 2>&1; then log_fatal "Impossible de se connecter au Mac mini (${MAC_MINI_HOST})" fi log_success "Connexion SSH établie" # Vérifier l'environnement Mac log_substep "Vérification de l'environnement Mac" MAC_INFO=$(ssh "$MAC_MINI_HOST" "sw_vers -productVersion && xcodebuild -version | head -1 && flutter --version | head -1" 2>/dev/null || echo "N/A") log_info "$(echo "$MAC_INFO" | head -1 | xargs -I {} echo " macOS: {}")" log_info "$(echo "$MAC_INFO" | sed -n '2p' | xargs -I {} echo " Xcode: {}")" log_info "$(echo "$MAC_INFO" | sed -n '3p' | xargs -I {} echo " Flutter: {}")" # ===================================== # Étape 5 : Transfert vers Mac mini # ===================================== log_step "ÉTAPE 5/8 : Transfert du projet vers Mac mini" DEST_DIR="${MAC_BASE_DIR}/app_${BUILD_NUMBER}" log_substep "Création du dossier de destination: ${DEST_DIR}" ssh_exec "mkdir -p ${DEST_DIR}" "Impossible de créer le dossier ${DEST_DIR} sur le Mac mini" log_substep "Transfert rsync (peut prendre 2-5 minutes)" TRANSFER_START=$(date +%s) rsync -avz --progress \ --exclude='build/' \ --exclude='.dart_tool/' \ --exclude='ios/Pods/' \ --exclude='ios/.symlinks/' \ --exclude='macos/Pods/' \ --exclude='linux/flutter/ephemeral/' \ --exclude='windows/flutter/ephemeral/' \ --exclude='android/build/' \ --exclude='*.aab' \ --exclude='*.apk' \ --exclude='logs/' \ --exclude='*.log' \ ./ "${MAC_MINI_HOST}:${DEST_DIR}/" >> "${LOG_FILE}" 2>&1 || log_fatal "Échec du transfert rsync" TRANSFER_TIME=$(($(date +%s) - TRANSFER_START)) log_success "Transfert terminé" log_info " Destination: ${DEST_DIR}" log_info " Durée: ${TRANSFER_TIME}s" # ===================================== # Étape 6 : Build et Archive avec Fastlane # ===================================== log_step "ÉTAPE 6/8 : Build et Archive iOS avec Fastlane" log_info "Cette étape peut prendre 15-25 minutes" log_info "Fastlane va :" log_info " 1. Nettoyer les artefacts" log_info " 2. Installer les CocoaPods" log_info " 3. Analyser le code" log_info " 4. Build Flutter iOS" log_info " 5. Archive Xcode (gym)" log_info " 6. Export IPA" log_info "" log_substep "Lancement de: cd ${DEST_DIR} && fastlane ios build" FASTLANE_START=$(date +%s) # Créer un fichier temporaire pour capturer la sortie Fastlane FASTLANE_LOG="/tmp/fastlane-ios-${TIMESTAMP}.log" # Exécuter Fastlane en temps réel avec affichage des étapes importantes ssh -t "$MAC_MINI_HOST" "cd ${DEST_DIR} && /opt/homebrew/bin/fastlane ios build" 2>&1 | tee -a "${LOG_FILE}" | tee "${FASTLANE_LOG}" | while IFS= read -r line; do # Afficher les lignes importantes if echo "$line" | grep -qE "(🧹|📦|🔧|🔍|🏗️|✓|✗|Error|error:|ERROR|Build succeeded|Build failed)"; then echo -e "${CYAN} ${line}${NC}" fi done # Vérifier le code de retour de Fastlane FASTLANE_EXIT_CODE=${PIPESTATUS[0]} FASTLANE_TIME=$(($(date +%s) - FASTLANE_START)) if [ $FASTLANE_EXIT_CODE -ne 0 ]; then log_error "Fastlane a échoué (code: ${FASTLANE_EXIT_CODE})" log_error "Analyse des erreurs..." # Extraire les erreurs du log Fastlane if [ -f "${FASTLANE_LOG}" ]; then log "" log "${RED}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" log "${RED}ERREURS DÉTECTÉES :${NC}" log "${RED}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" grep -i "error:\|Error:\|ERROR:\|❌\|✗" "${FASTLANE_LOG}" | head -20 | while IFS= read -r error_line; do log "${RED} ${error_line}${NC}" done log "${RED}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" fi log_fatal "Build iOS échoué via Fastlane. Consultez ${FASTLANE_LOG} pour plus de détails." fi log_success "Build et Archive iOS réussis" log_info " Durée totale Fastlane: ${FASTLANE_TIME}s ($((FASTLANE_TIME/60))m $((FASTLANE_TIME%60))s)" # Vérifier que l'IPA existe log_substep "Vérification de l'IPA généré" IPA_EXISTS=$(ssh "$MAC_MINI_HOST" "test -f ${DEST_DIR}/build/ios/ipa/Runner.ipa && echo 'YES' || echo 'NO'") if [ "$IPA_EXISTS" != "YES" ]; then log_fatal "IPA non trouvé dans ${DEST_DIR}/build/ios/ipa/Runner.ipa" fi IPA_SIZE=$(ssh "$MAC_MINI_HOST" "du -h ${DEST_DIR}/build/ios/ipa/Runner.ipa | cut -f1") log_info " IPA trouvé: ${GREEN}${IPA_SIZE}${NC}" # ===================================== # Étape 7 : Upload vers TestFlight (optionnel) # ===================================== log_step "ÉTAPE 7/8 : Upload vers TestFlight" log "" log_info "${YELLOW}Voulez-vous uploader l'IPA vers TestFlight maintenant ?${NC}" log_info " [Y] Oui - Upload automatique via fastlane ios upload" log_info " [N] Non - Je ferai l'upload manuellement plus tard" log "" read -p "$(echo -e ${CYAN}Votre choix [Y/n]: ${NC})" -n 1 -r UPLOAD_CHOICE echo log "" if [[ $UPLOAD_CHOICE =~ ^[Yy]$ ]] || [ -z "$UPLOAD_CHOICE" ]; then log_substep "Lancement de: fastlane ios upload" log_info "Upload vers TestFlight (peut prendre 5-10 minutes)" UPLOAD_START=$(date +%s) ssh -t "$MAC_MINI_HOST" "cd ${DEST_DIR} && /opt/homebrew/bin/fastlane ios upload" 2>&1 | tee -a "${LOG_FILE}" UPLOAD_EXIT_CODE=${PIPESTATUS[0]} UPLOAD_TIME=$(($(date +%s) - UPLOAD_START)) if [ $UPLOAD_EXIT_CODE -ne 0 ]; then log_error "Upload TestFlight échoué (code: ${UPLOAD_EXIT_CODE})" log_warning "L'IPA est disponible sur le Mac mini, vous pouvez réessayer manuellement" log_info " Commande: ssh $MAC_USER@$MAC_MINI_IP \"cd ${DEST_DIR} && fastlane ios upload\"" else log_success "Upload TestFlight réussi" log_info " Durée: ${UPLOAD_TIME}s" log_info " URL: ${CYAN}https://appstoreconnect.apple.com${NC}" fi else log_info "Upload ignoré. Pour uploader manuellement plus tard :" log_info " ${CYAN}ssh $MAC_MINI_HOST \"cd ${DEST_DIR} && /opt/homebrew/bin/fastlane ios upload\"${NC}" fi # ===================================== # Étape 8 : Nettoyage et archivage # ===================================== log_step "ÉTAPE 8/8 : Nettoyage et archivage" log_substep "Voulez-vous archiver le dossier de build ?" log_info " [Y] Oui - Créer une archive ${DEST_DIR}.tar.gz" log_info " [N] Non - Garder le dossier tel quel (défaut)" log "" read -p "$(echo -e ${CYAN}Votre choix [y/N]: ${NC})" -n 1 -r ARCHIVE_CHOICE echo log "" if [[ $ARCHIVE_CHOICE =~ ^[Yy]$ ]]; then log_substep "Création de l'archive..." ssh_exec "cd ${MAC_BASE_DIR} && tar -czf app_${BUILD_NUMBER}.tar.gz app_${BUILD_NUMBER}" \ "Échec de la création de l'archive" ARCHIVE_SIZE=$(ssh "$MAC_MINI_HOST" "du -h ${MAC_BASE_DIR}/app_${BUILD_NUMBER}.tar.gz | cut -f1") log_success "Archive créée" log_info " Archive: ${MAC_BASE_DIR}/app_${BUILD_NUMBER}.tar.gz (${ARCHIVE_SIZE})" log_substep "Suppression du dossier de build" ssh_exec "rm -rf ${DEST_DIR}" "Échec de la suppression du dossier" log_success "Dossier de build supprimé" else log_info "Dossier conservé: ${DEST_DIR}" fi # Restaurer le pubspec.yaml original (optionnel) log_substep "Restauration de pubspec.yaml local" mv pubspec.yaml.backup pubspec.yaml log_info " pubspec.yaml local restauré à son état initial" # ===================================== # Rapport final # ===================================== TOTAL_TIME=$(($(date +%s) - TOTAL_START_TIME)) TOTAL_MINUTES=$((TOTAL_TIME / 60)) TOTAL_SECONDS=$((TOTAL_TIME % 60)) log "" log "${GREEN}╔════════════════════════════════════════════════════════╗${NC}" log "${GREEN}║ ║${NC}" log "${GREEN}║ ✓ DÉPLOIEMENT iOS TERMINÉ AVEC SUCCÈS ║${NC}" log "${GREEN}║ ║${NC}" log "${GREEN}╚════════════════════════════════════════════════════════╝${NC}" log "" log "${CYAN}📊 RAPPORT DE DÉPLOIEMENT${NC}" log "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" log "" log " ${BLUE}Version déployée:${NC} ${GREEN}${VERSION} (Build ${BUILD_NUMBER})${NC}" log " ${BLUE}Destination:${NC} ${DEST_DIR}" log " ${BLUE}IPA généré:${NC} ${GREEN}${IPA_SIZE}${NC}" log " ${BLUE}Durée totale:${NC} ${GREEN}${TOTAL_MINUTES}m ${TOTAL_SECONDS}s${NC}" log "" if [ $WARNINGS_COUNT -gt 0 ]; then log " ${YELLOW}⚠ Avertissements:${NC} ${WARNINGS_COUNT}" fi if [ $ERRORS_COUNT -gt 0 ]; then log " ${RED}✗ Erreurs:${NC} ${ERRORS_COUNT}" fi log "" log "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" log "" log "${BLUE}📱 PROCHAINES ÉTAPES${NC}" log "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" log "" if [[ $UPLOAD_CHOICE =~ ^[Yy]$ ]] || [ -z "$UPLOAD_CHOICE" ]; then if [ $UPLOAD_EXIT_CODE -eq 0 ]; then log " 1. ${GREEN}✓${NC} IPA uploadé sur TestFlight" log " 2. Accéder à App Store Connect:" log " ${CYAN}https://appstoreconnect.apple.com${NC}" log " 3. Attendre le traitement Apple (5-15 min)" log " 4. Configurer la conformité export si demandée" log " 5. Ajouter des testeurs internes" log " 6. Installer via TestFlight sur iPhone" else log " 1. ${YELLOW}⚠${NC} Upload TestFlight a échoué" log " 2. Réessayer manuellement:" log " ${CYAN}ssh $MAC_USER@$MAC_MINI_IP \"cd ${DEST_DIR} && fastlane ios upload\"${NC}" fi else log " 1. L'IPA est prêt sur le Mac mini" log " 2. Pour uploader vers TestFlight:" log " ${CYAN}ssh $MAC_USER@$MAC_MINI_IP \"cd ${DEST_DIR} && fastlane ios upload\"${NC}" log " 3. Ou distribuer l'IPA manuellement via Xcode" fi log "" log "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" log "" log " ${BLUE}Log complet:${NC} ${LOG_FILE}" log " ${BLUE}Fin:${NC} $(date '+%Y-%m-%d %H:%M:%S')" log "" # Nettoyer le log Fastlane temporaire rm -f "${FASTLANE_LOG}" exit 0