Skip to content

Instantly share code, notes, and snippets.

@vijaybheda
Last active July 19, 2026 04:21
Show Gist options
  • Select an option

  • Save vijaybheda/7b6ef8dfb36d942014b1d688dfdd3dc1 to your computer and use it in GitHub Desktop.

Select an option

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.
#!/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
#!/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
@vijaybheda

Copy link
Copy Markdown
Author

🔑 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:

mkdir -p output/secrets

Make sure you add this line to your project's .gitignore file immediately so your production keys never leak online:

output/secrets/

2. Provision Your Private Credentials Assets

  • For iOS Deployment (xcrun altool):
  1. Generate an App Store Connect API key in your developer portal.
  2. Download the private key .p8 file.
  3. Move the key file into output/secrets/ and rename it to match the format: AuthKey_YOUR_APPLE_API_KEY_ID.p8.
  4. Update the variables APPLE_API_KEY_ID and APPLE_API_ISSUER_ID at the top of the scripts.
  • For Android Deployment (fastlane supply):
  1. Generate a Google Play Service Account JSON key via the Google Cloud / Google Play Console.
  2. Download the JSON credential package object.
  3. Save it inside the path 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.

@vijaybheda

Copy link
Copy Markdown
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.

  1. Log In: Go to [App Store Connect] and log in with an Account Holder or Admin profile.
  2. Navigate to Users and Access: Click on Users and Access, then select the Integrations tab at the top.
  3. Select App Store Connect API: In the left sidebar, ensure App Store Connect API is highlighted.
  4. Request Access (If first time): If your organization hasn't used the API before, click Request Access and accept the terms.
  5. 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.
  1. 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 .p8 file once. Save it directly to your output/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

  1. Log In: Open the [Google Cloud Console]. Ensure you are logged into the same Google account that owns your Google Play Console.
  2. Select/Create Project: Select your existing Google Play Android project from the top dropdown, or create a new one.
  3. Enable the API: Go to the API Library via the sidebar menu, search for Google Play Android Developer API, and click Enable.
  4. Navigate to Service Accounts: Go to IAM & Admin > Service Accounts in the left menu.
  5. 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.
  1. 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 .json file. Rename this file to google-play-service-account.json and move it into your output/secrets/ folder.

Part 2: Link the Account to your Google Play Console

  1. Open Play Console: Go to the Google Play Console and sign in as the Account Owner.
  2. 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).
  1. 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
  1. Send Invite: Click Invite user. Google Play will instantly link the permissions, and your JSON key file will immediately be authorized to push builds.

@vijaybheda

Copy link
Copy Markdown
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:

  1. Remove the Mandatory Flavor Guardrail Check:
    Inside Phase 0: Parameter & Safety Checks, comment out or delete the block checking if -f is 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
  2. 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"
  3. 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:

  1. The -f <flavor> flag to be passed explicitly every time you invoke the tool.
  2. 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