Last active
July 19, 2026 04:21
-
-
Save vijaybheda/7b6ef8dfb36d942014b1d688dfdd3dc1 to your computer and use it in GitHub Desktop.
Production-ready CLI scripts to automate, validate, and accelerate your Flutter build and App Store/Google Play deployment pipeline.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/bin/bash | |
| # ========================= TERMINAL UI CONFIG ========================= | |
| NC='\033[0m' | |
| BOLD='\033[1m' | |
| DIM='\033[2m' | |
| ITALIC='\033[3m' | |
| CYAN='\033[38;5;45m' | |
| GREEN='\033[38;5;84m' | |
| YELLOW='\033[38;5;221m' | |
| RED='\033[38;5;203m' | |
| MAGENTA='\033[38;5;207m' | |
| PURPLE='\033[38;5;99m' | |
| TICK="${GREEN}✔${NC}" | |
| CROSS="${RED}✘${NC}" | |
| INFO="${CYAN}ℹ${NC}" | |
| ARROW="${DIM}→${NC}" | |
| hide_cursor() { printf "\033[?25l"; } | |
| show_cursor() { printf "\033[?25h"; } | |
| cleanup() { show_cursor; echo -e "\n${YELLOW}⚠ Script terminated unexpectedly.${NC}"; exit 1; } | |
| trap cleanup SIGINT SIGTERM | |
| hide_cursor | |
| spin_engine() { | |
| local pid=$1 | |
| local delay=0.07 | |
| local spinstr='⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏' | |
| while [ "$(ps -p $pid -o state= 2>/dev/null)" ]; do | |
| local temp=${spinstr#?} | |
| printf " ${PURPLE}%c${NC} %s...\r" "$spinstr" "$2" | |
| local spinstr=$temp${spinstr%"$temp"} | |
| sleep $delay | |
| done | |
| wait $pid | |
| local exit_status=$? | |
| if [ $exit_status -eq 0 ]; then | |
| printf " ${TICK} %s\n" "$2" | |
| else | |
| printf " ${CROSS} ${RED}Failed: %s (Exit Code: %s)${NC}\n" "$2" "$exit_status" | |
| show_cursor | |
| exit $exit_status | |
| fi | |
| } | |
| # ========================= CORE CONFIGURATION ========================= | |
| # Change this prefix to match your application naming convention | |
| PREFIX="APP" | |
| OUTPUT_DIR="output" | |
| SECRETS_DIR="${OUTPUT_DIR}/secrets" | |
| IOS_DIR="${OUTPUT_DIR}/ios" | |
| ANDROID_DIR="${OUTPUT_DIR}/android" | |
| TIMESTAMP=$(date +"%I-%M_%p_%d_%b") | |
| FLAVOR="" | |
| BUILD_APK=false | |
| BUILD_AAB=false | |
| BUILD_IPA=false | |
| NOSIGN=false | |
| UPLOAD_PLAYSTORE=false | |
| UPLOAD_APPSTORE=false | |
| CLI_ANDROID_VERSION="" | |
| CLI_IOS_VERSION="" | |
| VALID_FLAVORS=("dev" "qa" "prod") | |
| # ----------------- PLACEHOLDER CREDENTIALS CONFIG ----------------- | |
| # Replace these strings with your actual App Store Connect configuration values | |
| APPLE_API_KEY_ID="YOUR_APPLE_API_KEY_ID" | |
| APPLE_API_ISSUER_ID="YOUR_APPLE_API_ISSUER_ID" | |
| APPLE_API_KEY_PATH="${SECRETS_DIR}/AuthKey_${APPLE_API_KEY_ID}.p8" | |
| # Replace this string with your Google Play Service Account JSON file name | |
| GOOGLE_PLAY_SERVICE_ACCOUNT_JSON="${SECRETS_DIR}/google-play-service-account.json" | |
| # ------------------------------------------------------------------ | |
| FINAL_ANDROID_ARTIFACT="" | |
| FINAL_IOS_ARTIFACT="" | |
| # ================= HELP FUNCTION ================= | |
| usage() { | |
| show_cursor | |
| echo -e "${BOLD}${CYAN}❖ ${PREFIX} BUILD SYSTEM INTERFACE${NC}" | |
| echo -e "${DIM}Automated compilation & store distribution orchestrator${NC}\n" | |
| echo -e "${BOLD}USAGE:${NC}" | |
| echo -e " ./build_release.sh [options]\n" | |
| echo -e "${BOLD}OPTIONS:${NC}" | |
| echo -e " ${CYAN}-f${NC} <flavor> Build variant (${ITALIC}dev, qa, prod${NC})" | |
| echo -e " ${CYAN}-p${NC} <platforms> Target space-separated options: ${BOLD}apk, appbundle, ipa, nosign${NC}" | |
| echo -e " ${CYAN}-v${NC} <versions> Explicit versions: ${BOLD}a1.0.0+1${NC} (Android), ${BOLD}i1.0.0+1${NC} (iOS)" | |
| echo -e " ${CYAN}-u${NC} <targets> Distribution targets: ${BOLD}playstore, appstore${NC}" | |
| echo -e " ${CYAN}-h, --help${NC} Render help options" | |
| exit 0 | |
| } | |
| if [[ "$#" -eq 0 ]]; then usage; fi | |
| # ================= ARGUMENT PARSING ================= | |
| while [[ "$#" -gt 0 ]]; do | |
| case $1 in | |
| -f) | |
| if [[ -z "$2" || "$2" =~ ^- ]]; then echo -e "${CROSS} ${RED}Error: -f requires a value.${NC}"; show_cursor; exit 1; fi | |
| FLAVOR="$2"; shift ;; | |
| -p) shift | |
| if [[ -z "$1" || "$1" =~ ^- ]]; then echo -e "${CROSS} ${RED}Error: -p requires at least one platform target.${NC}"; show_cursor; exit 1; fi | |
| while [[ "$#" -gt 0 && ! "$1" =~ ^- ]]; do | |
| case $1 in | |
| apk) BUILD_APK=true ;; | |
| appbundle) BUILD_AAB=true ;; | |
| ipa) BUILD_IPA=true ;; | |
| nosign) NOSIGN=true ;; | |
| *) echo -e "${CROSS} ${RED}Error: Invalid platform target '$1'. Allowed: apk, appbundle, ipa, nosign${NC}"; show_cursor; exit 1; ;; | |
| esac | |
| shift | |
| done | |
| continue ;; | |
| -v) shift | |
| if [[ -z "$1" || "$1" =~ ^- ]]; then echo -e "${CROSS} ${RED}Error: -v requires version parameters.${NC}"; show_cursor; exit 1; fi | |
| while [[ "$#" -gt 0 && ! "$1" =~ ^- ]]; do | |
| if [[ $1 == a* ]]; then CLI_ANDROID_VERSION="${1#a}"; | |
| elif [[ $1 == i* ]]; then CLI_IOS_VERSION="${1#i}"; | |
| else echo -e "${CROSS} ${RED}Error: Invalid version format '$1'. Must start with 'a' or 'i' (e.g., a1.0.0+1).${NC}"; show_cursor; exit 1; fi | |
| shift | |
| done | |
| continue ;; | |
| -u) shift | |
| if [[ -z "$1" || "$1" =~ ^- ]]; then echo -e "${CROSS} ${RED}Error: -u requires at least one upload target.${NC}"; show_cursor; exit 1; fi | |
| while [[ "$#" -gt 0 && ! "$1" =~ ^- ]]; do | |
| case $1 in | |
| playstore) UPLOAD_PLAYSTORE=true ;; | |
| appstore) UPLOAD_APPSTORE=true ;; | |
| *) echo -e "${CROSS} ${RED}Error: Invalid upload target '$1'. Allowed: playstore, appstore${NC}"; show_cursor; exit 1; ;; | |
| esac | |
| shift | |
| done | |
| continue ;; | |
| -h|--help) usage ;; | |
| *) echo -e "${CROSS} ${RED}Unknown option: $1${NC}"; usage ;; | |
| esac | |
| shift | |
| done | |
| # ================= PARAMETER & ENVIRONMENT VALIDATION ================= | |
| echo -e "${BOLD}🔍 Phase 0: Parameter & Safety Checks${NC}" | |
| if [ -n "$FLAVOR" ]; then | |
| MATCH=false | |
| for f in "${VALID_FLAVORS[@]}"; do | |
| if [[ "$f" == "$FLAVOR" ]]; then MATCH=true; fi | |
| done | |
| if [ "$MATCH" = false ]; then | |
| echo -e " ${CROSS} ${RED}Error: Invalid flavor '${FLAVOR}'. Allowed options: ${VALID_FLAVORS[*]}${NC}" | |
| show_cursor; exit 1 | |
| fi | |
| else | |
| echo -e " ${CROSS} ${RED}Error: Missing mandatory flavor parameter (-f).${NC}" | |
| show_cursor; exit 1 | |
| fi | |
| if [ "$BUILD_APK" = false ] && [ "$BUILD_AAB" = false ] && [ "$BUILD_IPA" = false ]; then | |
| echo -e " ${CROSS} ${RED}Error: You must specify at least one compilation platform package target (-p apk|appbundle|ipa).${NC}" | |
| show_cursor; exit 1 | |
| fi | |
| if [ "$UPLOAD_PLAYSTORE" = true ] && [ "$BUILD_APK" = false ] && [ "$BUILD_AAB" = false ]; then | |
| echo -e " ${CROSS} ${RED}Logic Mismatch: Cannot upload to Play Store without selecting 'apk' or 'appbundle' to build.${NC}" | |
| show_cursor; exit 1 | |
| fi | |
| if [ "$UPLOAD_APPSTORE" = true ] && [ "$BUILD_IPA" = false ]; then | |
| echo -e " ${CROSS} ${RED}Logic Mismatch: Cannot upload to App Store without selecting 'ipa' to build.${NC}" | |
| show_cursor; exit 1 | |
| fi | |
| if [ "$UPLOAD_PLAYSTORE" = true ] && [ ! -f "$GOOGLE_PLAY_SERVICE_ACCOUNT_JSON" ]; then | |
| echo -e " ${CROSS} ${RED}Configuration Fault: Google Play Service Account JSON missing at $GOOGLE_PLAY_SERVICE_ACCOUNT_JSON${NC}" | |
| show_cursor; exit 1 | |
| fi | |
| if [ "$UPLOAD_APPSTORE" = true ] && [ ! -f "$APPLE_API_KEY_PATH" ]; then | |
| echo -e " ${CROSS} ${RED}Configuration Fault: iOS .p8 Key file missing at $APPLE_API_KEY_PATH${NC}" | |
| show_cursor; exit 1 | |
| fi | |
| echo -e " ${TICK} Parameters and configuration profiles validated successfully." | |
| # ================= CONFIG DYNAMICS & BUNDLING ================= | |
| # Dynamic Package Name assignment based on target flavor | |
| if [ "$FLAVOR" == "prod" ]; then | |
| ANDROID_PACKAGE_NAME="com.yourdomain.app" | |
| IOS_BUNDLE_ID="com.yourdomain.app" | |
| else | |
| ANDROID_PACKAGE_NAME="com.yourdomain.app.${FLAVOR}" | |
| IOS_BUNDLE_ID="com.yourdomain.app.${FLAVOR}" | |
| fi | |
| PUBSPEC_FILE="pubspec.yaml" | |
| if [ ! -f "$PUBSPEC_FILE" ]; then | |
| echo -e "${CROSS} ${RED}Critical Failure: 'pubspec.yaml' missing.${NC}" | |
| show_cursor && exit 1 | |
| fi | |
| DEFAULT_VERSION=$(grep '^version:' $PUBSPEC_FILE | awk '{print $2}') | |
| ANDROID_RAW=$(grep '^# Android version:' $PUBSPEC_FILE | awk '{print $4}') | |
| IOS_RAW=$(grep '^# iOS version:' $PUBSPEC_FILE | awk '{print $4}') | |
| ANDROID_FINAL=${CLI_ANDROID_VERSION:-${ANDROID_RAW:-$DEFAULT_VERSION}} | |
| IOS_FINAL=${CLI_IOS_VERSION:-${IOS_RAW:-$DEFAULT_VERSION}} | |
| split_version() { | |
| local full=$1 | |
| echo "${full%+*}" "${full#*+}" | |
| } | |
| read ANDROID_NAME ANDROID_CODE <<< $(split_version $ANDROID_FINAL) | |
| read IOS_NAME IOS_CODE <<< $(split_version $IOS_FINAL) | |
| FLAVOR_ARGS="--flavor $FLAVOR" | |
| DART_DEFINE_ARGS="--dart-define=FLAVOR=$FLAVOR" | |
| FLAVOR_LABEL=$FLAVOR | |
| mkdir -p "${IOS_DIR}" "${ANDROID_DIR}" | |
| # ================= PRINT PIPELINE RUNTIME METRICS ARCHITECTURE ================= | |
| echo -e "\n${BOLD}${CYAN}⚙ PIPELINE PRE-FLIGHT DASHBOARD${NC}" | |
| echo -e "${DIM}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" | |
| echo -e " Environment Flavor: ${MAGENTA}${FLAVOR_LABEL}${NC}" | |
| echo -e " Android Identifier: ${BOLD}${ANDROID_PACKAGE_NAME}${NC} (${YELLOW}v$ANDROID_FINAL${NC})" | |
| echo -e " iOS Bundle ID: ${BOLD}${IOS_BUNDLE_ID}${NC} (${YELLOW}v$IOS_FINAL${NC})" | |
| echo -e " Execution Tasks: ${GREEN}Compile -> $( [ "$BUILD_APK" = true ] && printf "APK " )$( [ "$BUILD_AAB" = true ] && printf "AAB " )$( [ "$BUILD_IPA" = true ] && printf "IPA " )${NC}" | |
| echo -e " Deployment Distribution: ${CYAN}$( [ "$UPLOAD_PLAYSTORE" = true ] && printf "PlayStore " )$( [ "$UPLOAD_APPSTORE" = true ] && printf "AppStore " )$(([ "$UPLOAD_PLAYSTORE" != true ] && [ "$UPLOAD_APPSTORE" != true ]) && printf "None")${NC}" | |
| echo -e "${DIM}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}\n" | |
| # ================= WORKSPACE OPERATION EXECUTIONS ================= | |
| echo -e "${BOLD}📦 Phase 1: Workspace Sanitation${NC}" | |
| flutter clean > /dev/null 2>&1 & | |
| spin_engine $! "Purging build telemetry and project caches" | |
| flutter pub get > /dev/null 2>&1 & | |
| spin_engine $! "Resolving remote pub dependency tree mappings" | |
| echo -e "\n${BOLD}🛠 Phase 2: Platform Engine Compilation Pipeline${NC}" | |
| build_android() { | |
| if [ "$BUILD_APK" = true ]; then | |
| flutter build apk --release $FLAVOR_ARGS $DART_DEFINE_ARGS --build-name=${ANDROID_NAME} --build-number=${ANDROID_CODE} > /dev/null 2>&1 & | |
| spin_engine $! "Compiling Android Release Application Package (APK)" | |
| APK_SRC=$(find build/app/outputs/flutter-apk -name "*.apk" ! -name "*unaligned*" | head -n 1) | |
| if [ -f "$APK_SRC" ]; then | |
| FINAL_ANDROID_ARTIFACT="${ANDROID_DIR}/${PREFIX}_${FLAVOR_LABEL}_${ANDROID_NAME}_${ANDROID_CODE}_${TIMESTAMP}.apk" | |
| cp "$APK_SRC" "$FINAL_ANDROID_ARTIFACT" | |
| echo -e " ${ARROW} Saved: ${DIM}${FINAL_ANDROID_ARTIFACT}${NC}" | |
| fi | |
| fi | |
| if [ "$BUILD_AAB" = true ]; then | |
| flutter build appbundle --release $FLAVOR_ARGS $DART_DEFINE_ARGS --build-name=${ANDROID_NAME} --build-number=${ANDROID_CODE} > /dev/null 2>&1 & | |
| spin_engine $! "Compiling Production AppBundle Distribution Payload (AAB)" | |
| AAB_SRC=$(find build/app/outputs/bundle -name "*.aab" | head -n 1) | |
| if [ -f "$AAB_SRC" ]; then | |
| FINAL_ANDROID_ARTIFACT="${ANDROID_DIR}/${PREFIX}_${FLAVOR_LABEL}_${ANDROID_NAME}_${ANDROID_CODE}_${TIMESTAMP}.aab" | |
| cp "$AAB_SRC" "$FINAL_ANDROID_ARTIFACT" | |
| echo -e " ${ARROW} Saved: ${DIM}${FINAL_ANDROID_ARTIFACT}${NC}" | |
| fi | |
| fi | |
| } | |
| build_ios() { | |
| EXTRA_FLAG="" | |
| [ "$NOSIGN" = true ] && EXTRA_FLAG="--no-codesign" | |
| flutter build ipa --release $EXTRA_FLAG $FLAVOR_ARGS $DART_DEFINE_ARGS --build-name=${IOS_NAME} --build-number=${IOS_CODE} > /dev/null 2>&1 & | |
| spin_engine $! "Compiling iOS Archive & Provisioning Native Packages (IPA)" | |
| if [ "$NOSIGN" = false ]; then | |
| IPA_SRC=$(find build/ios/ipa -name "*.ipa" | head -n 1) | |
| if [ -f "$IPA_SRC" ]; then | |
| FINAL_IOS_ARTIFACT="${IOS_DIR}/${PREFIX}_${FLAVOR_LABEL}_${IOS_NAME}_${IOS_CODE}_${TIMESTAMP}.ipa" | |
| cp "$IPA_SRC" "$FINAL_IOS_ARTIFACT" | |
| echo -e " ${ARROW} Saved: ${DIM}${FINAL_IOS_ARTIFACT}${NC}" | |
| fi | |
| fi | |
| } | |
| if [ "$BUILD_APK" = true ] || [ "$BUILD_AAB" = true ]; then build_android; fi | |
| if [ "$BUILD_IPA" = true ]; then build_ios; fi | |
| # ================= UPLOAD FUNCTIONS ================= | |
| echo -e "\n${BOLD}🚀 Phase 3: Gateway Release and Store Distribution${NC}" | |
| upload_android_task() { | |
| if [ "$UPLOAD_PLAYSTORE" = true ] && [ -n "$FINAL_ANDROID_ARTIFACT" ]; then | |
| if command -v fastlane &> /dev/null; then | |
| local flag="--aab" | |
| [[ "$FINAL_ANDROID_ARTIFACT" == *.apk ]] && flag="--apk" | |
| fastlane supply --package_name "$ANDROID_PACKAGE_NAME" "$flag" "$FINAL_ANDROID_ARTIFACT" --track "internal" --json_key "$GOOGLE_PLAY_SERVICE_ACCOUNT_JSON" > /dev/null 2>&1 & | |
| spin_engine $! "Uploading binary artifact to Google Play Internal Track" | |
| else | |
| echo -e " ${CROSS} ${RED}Error: Local dependency environment validation missed 'fastlane'.${NC}" | |
| fi | |
| fi | |
| } | |
| upload_ios_task() { | |
| if [ "$UPLOAD_APPSTORE" = true ] && [ -n "$FINAL_IOS_ARTIFACT" ]; then | |
| xcrun altool --upload-app --type ios -f "$FINAL_IOS_ARTIFACT" --apiKey "$APPLE_API_KEY_ID" --apiIssuer "$APPLE_API_ISSUER_ID" --apiKeyPath "$APPLE_API_KEY_PATH" > /dev/null 2>&1 & | |
| spin_engine $! "Uploading binary payload asset packages securely to TestFlight" | |
| fi | |
| } | |
| upload_android_task | |
| upload_ios_task | |
| # ================= FINALIZATION ================= | |
| echo -e "\n${BOLD}${GREEN}🎉 PIPELINE COMPLETE SUITE SUCCEEDED${NC}" | |
| echo -e "${DIM}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" | |
| echo -e " ${INFO} Local Outputs Matrix Location Details:" | |
| echo -e " ${ARROW} Android Storage Repository Vault: ${BOLD}${ANDROID_DIR}${NC}" | |
| echo -e " ${ARROW} Apple Mobile Storage System Path: ${BOLD}${IOS_DIR}${NC}" | |
| if [ "$BUILD_IPA" = true ] && [ "$NOSIGN" = false ]; then | |
| ARCHIVE="build/ios/archive/Runner.xcarchive" | |
| [ -d "$ARCHIVE" ] && open "$ARCHIVE" | |
| fi | |
| echo -e "${DIM}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}\n" | |
| show_cursor |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/bin/bash | |
| # ========================= TERMINAL UI CONFIG ========================= | |
| NC='\033[0m' | |
| BOLD='\033[1m' | |
| DIM='\033[2m' | |
| ITALIC='\033[3m' | |
| CYAN='\033[38;5;45m' | |
| GREEN='\033[38;5;84m' | |
| YELLOW='\033[38;5;221m' | |
| RED='\033[38;5;203m' | |
| MAGENTA='\033[38;5;207m' | |
| PURPLE='\033[38;5;99m' | |
| TICK="${GREEN}✔${NC}" | |
| CROSS="${RED}✘${NC}" | |
| INFO="${CYAN}ℹ${NC}" | |
| ARROW="${DIM}→${NC}" | |
| hide_cursor() { printf "\033[?25l"; } | |
| show_cursor() { printf "\033[?25h"; } | |
| cleanup() { show_cursor; echo -e "\n${YELLOW}⚠ Pipeline aborted by operator request.${NC}"; exit 1; } | |
| trap cleanup SIGINT SIGTERM | |
| hide_cursor | |
| spin_engine() { | |
| local pid=$1 | |
| local delay=0.07 | |
| local spinstr='⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏' | |
| while [ "$(ps -p $pid -o state= 2>/dev/null)" ]; do | |
| local temp=${spinstr#?} | |
| printf " ${PURPLE}%c${NC} %s...\r" "$spinstr" "$2" | |
| local spinstr=$temp${spinstr%"$temp"} | |
| sleep $delay | |
| done | |
| wait $pid | |
| local exit_status=$? | |
| if [ $exit_status -eq 0 ]; then | |
| printf " ${TICK} %s\n" "$2" | |
| else | |
| printf " ${CROSS} ${RED}Deployment failed: %s (Status: %s)${NC}\n" "$2" "$exit_status" | |
| show_cursor | |
| exit $exit_status | |
| fi | |
| } | |
| # ========================= CONFIG ========================= | |
| FLAVOR="" | |
| ANDROID_PATH="" | |
| IOS_PATH="" | |
| UPLOAD_PLAYSTORE=false | |
| UPLOAD_APPSTORE=false | |
| VALID_FLAVORS=("dev" "qa" "prod") | |
| SECRETS_DIR="./output/secrets" | |
| # ----------------- PLACEHOLDER CREDENTIALS CONFIG ----------------- | |
| APPLE_API_KEY_ID="YOUR_APPLE_API_KEY_ID" | |
| APPLE_API_ISSUER_ID="YOUR_APPLE_API_ISSUER_ID" | |
| APPLE_API_KEY_PATH="${SECRETS_DIR}/AuthKey_${APPLE_API_KEY_ID}.p8" | |
| GOOGLE_PLAY_SERVICE_ACCOUNT_JSON="${SECRETS_DIR}/google-play-service-account.json" | |
| # ------------------------------------------------------------------ | |
| # ================= HELP FUNCTION ================= | |
| usage() { | |
| show_cursor | |
| echo -e "${BOLD}${MAGENTA}🚀 STANDALONE MOUNT DISTRIBUTION ENGINE${NC}" | |
| echo -e "${DIM}Direct out-of-band artifact dispatch interface bundle${NC}\n" | |
| echo -e "${BOLD}USAGE:${NC}" | |
| echo -e " ./store_upload.sh [options]\n" | |
| echo -e "${BOLD}OPTIONS:${NC}" | |
| echo -e " ${MAGENTA}-f${NC} <flavor> Config Target Profile Variant" | |
| echo -e " ${MAGENTA}--android${NC} <path> File location for package object (.apk/.aab)" | |
| echo -e " ${MAGENTA}--ios${NC} <path> System location path for (.ipa)" | |
| echo -e " ${MAGENTA}-u${NC} <targets> Upload channels: ${BOLD}playstore, appstore${NC}" | |
| echo -e " ${MAGENTA}-h, --help${NC} Render documentation" | |
| exit 0 | |
| } | |
| if [[ "$#" -eq 0 ]]; then usage; fi | |
| # ================= ARGUMENT PARSING ================= | |
| while [[ "$#" -gt 0 ]]; do | |
| case $1 in | |
| -f) | |
| if [[ -z "$2" || "$2" =~ ^- ]]; then echo -e "${CROSS} ${RED}Error: -f requires a value.${NC}"; show_cursor; exit 1; fi | |
| FLAVOR="$2"; shift ;; | |
| --android) | |
| if [[ -z "$2" || "$2" =~ ^- ]]; then echo -e "${CROSS} ${RED}Error: --android requires a valid file path value.${NC}"; show_cursor; exit 1; fi | |
| ANDROID_PATH="$2"; shift ;; | |
| --ios) | |
| if [[ -z "$2" || "$2" =~ ^- ]]; then echo -e "${CROSS} ${RED}Error: --ios requires a valid file path value.${NC}"; show_cursor; exit 1; fi | |
| IOS_PATH="$2"; shift ;; | |
| -u) shift | |
| if [[ -z "$1" || "$1" =~ ^- ]]; then echo -e "${CROSS} ${RED}Error: -u requires at least one distribution target.${NC}"; show_cursor; exit 1; fi | |
| while [[ "$#" -gt 0 && ! "$1" =~ ^- ]]; do | |
| case $1 in | |
| playstore) UPLOAD_PLAYSTORE=true ;; | |
| appstore) UPLOAD_APPSTORE=true ;; | |
| *) echo -e "${CROSS} ${RED}Error: Invalid target '$1'. Allowed: playstore, appstore${NC}"; show_cursor; exit 1; ;; | |
| esac | |
| shift | |
| done | |
| continue ;; | |
| -h|--help) usage ;; | |
| *) echo -e "${CROSS} ${RED}Unknown dynamic parameter: $1${NC}"; usage ;; | |
| esac | |
| shift | |
| done | |
| # ================= PARAMETER & PATH SANITY VALIDATION ================= | |
| echo -e "${BOLD}🔍 Phase 0: Operational Checkouts & Guardrails${NC}" | |
| if [ "$UPLOAD_PLAYSTORE" = false ] && [ "$UPLOAD_APPSTORE" = false ]; then | |
| echo -e " ${CROSS} ${RED}Error: You must pick at least one deployment dispatch channel (-u playstore|appstore).${NC}" | |
| show_cursor; exit 1 | |
| fi | |
| if [ -n "$FLAVOR" ]; then | |
| MATCH=false | |
| for f in "${VALID_FLAVORS[@]}"; do | |
| if [[ "$f" == "$FLAVOR" ]]; then MATCH=true; fi | |
| done | |
| if [ "$MATCH" = false ]; then | |
| echo -e " ${CROSS} ${RED}Error: Invalid target profile configuration '${FLAVOR}'. Supported: ${VALID_FLAVORS[*]}${NC}" | |
| show_cursor; exit 1 | |
| fi | |
| fi | |
| if [ "$UPLOAD_PLAYSTORE" = true ]; then | |
| if [ -z "$ANDROID_PATH" ]; then | |
| echo -e " ${CROSS} ${RED}Error: Flag requested 'playstore' upload, but no --android path target was passed.${NC}" | |
| show_cursor; exit 1 | |
| elif [ ! -f "$ANDROID_PATH" ]; then | |
| echo -e " ${CROSS} ${RED}Error: Specified Android file targets do not exist at: $ANDROID_PATH${NC}" | |
| show_cursor; exit 1 | |
| fi | |
| fi | |
| if [ "$UPLOAD_APPSTORE" = true ]; then | |
| if [ -z "$IOS_PATH" ]; then | |
| echo -e " ${CROSS} ${RED}Error: Flag requested 'appstore' upload, but no --ios path target was passed.${NC}" | |
| show_cursor; exit 1 | |
| elif [ ! -f "$IOS_PATH" ]; then | |
| echo -e " ${CROSS} ${RED}Error: Specified iOS archive payload asset does not exist at: $IOS_PATH${NC}" | |
| show_cursor; exit 1 | |
| fi | |
| fi | |
| echo -e " ${TICK} Binary asset targets verified on system volume mapping." | |
| if [ "$FLAVOR" == "prod" ]; then | |
| ANDROID_PACKAGE_NAME="com.yourdomain.app" | |
| IOS_BUNDLE_ID="com.yourdomain.app" | |
| else | |
| ANDROID_PACKAGE_NAME="com.yourdomain.app.${FLAVOR}" | |
| IOS_BUNDLE_ID="com.yourdomain.app.${FLAVOR}" | |
| fi | |
| # ================= RUNTIME PREVIEW METRICS INTERFACE ================= | |
| echo -e "\n${BOLD}${MAGENTA}📡 DISTRIBUTION OUT-OF-BAND ENGINE PIPELINE${NC}" | |
| echo -e "${DIM}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" | |
| printf " %-22s %s\n" "Pipeline Identity Variant:" "${CYAN}${FLAVOR:-default}${NC}" | |
| printf " %-22s %s\n" "Android Binary Target:" "${BOLD}${ANDROID_PATH:-[Skipped Content Parameters]}${NC}" | |
| printf " %-22s %s\n" "iOS Application Target:" "${BOLD}${IOS_PATH:-[Skipped Content Parameters]}${NC}" | |
| echo -e "${DIM}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}\n" | |
| # ================= UPLOAD TASKS ================= | |
| echo -e "${BOLD}📤 Execution Dispatch Node Action Matrix${NC}" | |
| if [ "$UPLOAD_PLAYSTORE" = true ] && [ -f "$ANDROID_PATH" ]; then | |
| if [ ! -f "$GOOGLE_PLAY_SERVICE_ACCOUNT_JSON" ]; then | |
| echo -e " ${CROSS} ${RED}Credentials token payload absent: $GOOGLE_PLAY_SERVICE_ACCOUNT_JSON${NC}" | |
| elif command -v fastlane &> /dev/null; then | |
| flag="--aab" | |
| [[ "$ANDROID_PATH" == *.apk ]] && flag="--apk" | |
| fastlane supply --package_name "$ANDROID_PACKAGE_NAME" "$flag" "$ANDROID_PATH" --track "internal" --json_key "$GOOGLE_PLAY_SERVICE_ACCOUNT_JSON" > /dev/null 2>&1 & | |
| spin_engine $! "Dispatching Google Play Distribution Package Build Binary to Internal Store Channels" | |
| else | |
| echo -e " ${CROSS} ${RED}System Command Environment Dependency Missing Target: 'fastlane'.${NC}" | |
| fi | |
| fi | |
| if [ "$UPLOAD_APPSTORE" = true ] && [ -f "$IOS_PATH" ]; then | |
| if [ ! -f "$APPLE_API_KEY_PATH" ]; then | |
| echo -e " ${CROSS} ${RED}Identity key vault map asset missing at: $APPLE_API_KEY_PATH${NC}" | |
| else | |
| xcrun altool --upload-app --type ios -f "$IOS_PATH" --apiKey "$APPLE_API_KEY_ID" --apiIssuer "$APPLE_API_ISSUER_ID" --apiKeyPath "$APPLE_API_KEY_PATH" > /dev/null 2>&1 & | |
| spin_engine $! "Dispatching Signed iOS Application Binary Distribution Archive Asset directly to TestFlight" | |
| fi | |
| fi | |
| echo -e "\n${BOLD}${GREEN}✨ DISPATCH ROUTINES COMPLETED${NC}" | |
| echo -e "${DIM}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" | |
| echo -e " All remote application distribution pipeline state checks concluded successfully." | |
| echo -e "${DIM}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}\n" | |
| show_cursor |
Author
Author
🍏 How to Generate the iOS .p8 API Key
App Store Connect uses API keys to authenticate command-line tools like xcrun altool without requiring your primary password or 2FA prompts.
- Log In: Go to [App Store Connect] and log in with an Account Holder or Admin profile.
- Navigate to Users and Access: Click on Users and Access, then select the Integrations tab at the top.
- Select App Store Connect API: In the left sidebar, ensure App Store Connect API is highlighted.
- Request Access (If first time): If your organization hasn't used the API before, click Request Access and accept the terms.
- Generate API Key:
- Click the
+(Add) icon next to the Active keys header. - Provide a clear name (e.g.,
CLI Deployment Key). - Assign the role: Choose App Manager or Admin (required for app uploads).
- Click Generate.
- Download and Record:
- Copy the Issuer ID displayed at the top of the page (this goes into
APPLE_API_ISSUER_ID). - Find your new key in the list, copy the Key ID (this goes into
APPLE_API_KEY_ID). - Click Download API Key.
- Note: You can only download this
.p8file once. Save it directly to youroutput/secrets/directory.
🤖 How to Generate the google-play-service-account.json
Google Play uses service account tokens to let tools like Fastlane securely interact with your console releases.
Part 1: Create the API Project & Service Account
- Log In: Open the [Google Cloud Console]. Ensure you are logged into the same Google account that owns your Google Play Console.
- Select/Create Project: Select your existing Google Play Android project from the top dropdown, or create a new one.
- Enable the API: Go to the API Library via the sidebar menu, search for Google Play Android Developer API, and click Enable.
- Navigate to Service Accounts: Go to IAM & Admin > Service Accounts in the left menu.
- Create Service Account:
- Click Create Service Account at the top.
- Provide an explicit name (e.g.,
play-store-uploader). - Click Create and Continue.
- Grant Access (Role): Select the role Service Accounts > Service Account User (or project Owner/Editor). Click Continue, then Done.
- Generate the JSON Key:
- Click on the newly created service account email string from the list.
- Go to the Keys tab at the top.
- Click Add Key > Create new key.
- Select JSON as the format and click Create.
- Your browser will automatically download a
.jsonfile. Rename this file togoogle-play-service-account.jsonand move it into youroutput/secrets/folder.
Part 2: Link the Account to your Google Play Console
- Open Play Console: Go to the Google Play Console and sign in as the Account Owner.
- Invite the Service Account:
- Navigate to Users and permissions in the sidebar menu.
- Click Invite new users.
- Paste the email address of the Service Account you just generated in Google Cloud (e.g.,
play-store-uploader@your-project.iam.gserviceaccount.com).
- Set App Permissions:
- Go to the Account permissions tab or choose Add app to explicitly select the specific Android app target.
- Ensure the following permissions are explicitly toggled On:
- View app information and download reports (Read only)
- Manage production releases, studio releases, and test tracks
- Edit and delete draft apps
- Send Invite: Click Invite user. Google Play will instantly link the permissions, and your JSON key file will immediately be authorized to push builds.
Author
🚀 CLI Usage Matrix & Flavor Configuration Guide
This guide details how to execute both scripts under various pipeline demands, along with how to manage commands if your application doesn't use Flutter flavors.
1. Script Execution Examples
🔹 build_release.sh (Compile & Deploy Pipeline)
- Full Production Run (Android AAB + iOS IPA + Dual Store Push):
./build_release.sh -p appbundle ipa -f prod -v a1.0.0+10 i1.0.0+10 -u playstore appstore
- Local QA Testing Run (Android APK + Local iOS IPA, No Store Upload):
./build_release.sh -p apk ipa -f qa -v a1.2.0+4 i1.2.0+4
- Unsigned iOS Build Only (Fast compilation for export checks):
./build_release.sh -p ipa nosign -f dev
🔹 store_upload.sh (Standalone Out-of-Band Dispatches)
- Push Pre-compiled Binaries directly to both App Stores:
./store_upload.sh -f prod --android ./output/android/APP_prod_1.0.0_10.aab --ios ./output/ios/APP_prod_1.0.0_10.ipa -u playstore appstore
- Android-Only Internal Track Push:
./store_upload.sh -f qa --android ./output/android/APP_qa_1.1.0_12.apk -u playstore
2. Tailoring the Scripts For Flavor vs. Non-Flavor Layouts
Depending on your architecture, you may need to strip out or retain specific configurations in the scripts.
🚫 If Your App Has NO Flavors (Standard Project Setup)
If your app uses a single target configuration without separate dev/qa/prod environments, apply these changes:
- Remove the Mandatory Flavor Guardrail Check:
InsidePhase 0: Parameter & Safety Checks, comment out or delete the block checking if-fis empty:# Remove or comment out this block: # if [ -z "$FLAVOR" ]; then # echo -e " ${CROSS} ${RED}Error: Missing mandatory flavor parameter (-f).${NC}" # show_cursor; exit 1 # fi
- Simplify Application ID Assignments:
Change the dynamic package block under# ================= CONFIG DYNAMICS & BUNDLING =================to use your static bundle IDs explicitly:ANDROID_PACKAGE_NAME="com.yourcompany.yourapp" IOS_BUNDLE_ID="com.yourcompany.yourapp" FLAVOR_LABEL="default"
- Clear the Build Variable Arguments:
Ensure the build arguments sent down to the compiler engine run clean without appending flavor flags:FLAVOR_ARGS="" DART_DEFINE_ARGS=""
⚙️ If Your App USES Flavors (Multi-Environment Setup)
Keep the scripts exactly as they are written above. The scripts natively expect:
- The
-f <flavor>flag to be passed explicitly every time you invoke the tool. - Dynamic Package Matching: The script automatically appends the flavor identifier suffix to your bundle ID (e.g.,
com.yourcompany.yourapp.qa) for non-production environments to match native App Store/Play Store parallel testing track guidelines.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
🔑 Directory Architecture & Keys Configuration Guide
To use these automation scripts, you need to establish a local directory structure to keep your private access keys safe and out of git tracking.
1. Setup Your Folder Structure
Create an
output/secrets/directory in your Flutter project root layout:Make sure you add this line to your project's
.gitignorefile immediately so your production keys never leak online:2. Provision Your Private Credentials Assets
xcrun altool):.p8file.output/secrets/and rename it to match the format:AuthKey_YOUR_APPLE_API_KEY_ID.p8.APPLE_API_KEY_IDandAPPLE_API_ISSUER_IDat the top of the scripts.fastlane supply):output/secrets/google-play-service-account.json.3. Update the Target Application IDs
Open the scripts and quickly customize the dynamic package strings (
com.yourdomain.app) under the# ================= CONFIG DYNAMICS & BUNDLING =================header block to fit your specific build identifiers.