How to Set Up Xcode Cloud for Flutter: Automated App Store & TestFlight CI/CD

Flutter Xcode Cloud CI/CD Pipeline Architecture Diagram - Automated TestFlight and App Store Deployment

Figure 1: Automated Flutter CI/CD pipeline architecture using Apple Xcode Cloud and TestFlight.

Flutter CI/CD Apple Xcode Cloud App Store & TestFlight

Tired of managing Base64-encoded P12 certificates, expiring provisioning profiles, and runner keychains in third-party CI/CD tools? Here is the complete, high-performance guide to setting up Apple Xcode Cloud for Flutter—building, signing, and shipping directly to TestFlight and the App Store automatically on every push to production.

Why Choose Xcode Cloud for Flutter?

Deploying iOS apps using traditional CI/CD platforms (like GitHub Actions, GitLab CI, or Bitrise) introduces what mobile engineers call the "Certificate Tax". You have to export distribution certificates, set passwords, encode provisioning profiles, and script temporary macOS keychains. When a certificate expires, your deployments break.

Apple Xcode Cloud eliminates this friction completely. Because it runs directly inside Apple's ecosystem:

  • 100% Apple-Managed Code Signing: No P12 files, no provisioning profiles, and zero certificates to manage.
  • Zero Secret Management: No sensitive store upload API keys stored in third-party vaults.
  • Direct Ingestion: Builds are archived and uploaded directly to App Store Connect and distributed to TestFlight instantly.
  • Fast Compilation: Runs natively on Apple Silicon infrastructure.
Capability GitHub Actions / Fastlane Apple Xcode Cloud
Signing Certificates Manual export (.p12), passwords & profiles Automated by Apple
Secret Storage High (API keys, Base64 strings, keychains) Zero Secrets Required
TestFlight Ingestion Requires external upload script & API token Instant Native Ingestion
Maintenance Annual certificate renews & breakage Zero Maintenance

The Core Challenge: Flutter on Xcode Cloud

Xcode Cloud macOS runners come with Xcode, Homebrew, and Ruby pre-installed, but the Flutter SDK is not installed by default.

To make Flutter work on Xcode Cloud, Apple provides custom lifecycle build scripts. By placing an executable shell script named ci_post_clone.sh inside a ci_scripts folder, Xcode Cloud automatically runs it right after cloning your repository—allowing us to install Flutter, fetch dependencies, and generate iOS build configurations in a single pass.

📌 Where to Place the Script
Place the script inside airuni_connect_flutter/ios/ci_scripts/ci_post_clone.sh (or ios/ci_scripts/ci_post_clone.sh) and create a mirror at your repository root: ci_scripts/ci_post_clone.sh.

Production-Ready ci_post_clone.sh Script

Below is the complete, speed-optimized script. It features single-pass dependency resolution, shallow single-branch cloning (~15s), Homebrew auto-update suppression, and a configurable boolean toggle (USE_INFO_PLIST_VERSION) to choose whether your app version comes from pubspec.yaml or Info.plist:

ios/ci_scripts/ci_post_clone.sh
#!/bin/sh

# Fail script immediately if any command returns a non-zero exit status
set -e

# ==========================================================
#  CONFIGURATION: Version Source Selector
#  - Set to 'true'  -> Uses ios/Runner/Info.plist for Version & Build Number
#  - Set to 'false' -> Uses pubspec.yaml for Version & Build Number
#  (Can also be overridden by Xcode Cloud Environment Variables)
# ==========================================================
USE_INFO_PLIST_VERSION="${USE_INFO_PLIST_VERSION:-false}"

echo "=========================================================="
echo "  AirUni Connect — Xcode Cloud Post-Clone Build Hook      "
echo "=========================================================="

# 1. Resolve repository and Flutter project directories
if [ -n "$CI_PRIMARY_REPOSITORY_PATH" ]; then
  REPO_ROOT="$CI_PRIMARY_REPOSITORY_PATH"
else
  SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
  REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
fi

if [ -d "$REPO_ROOT/airuni_connect_flutter" ]; then
  FLUTTER_DIR="$REPO_ROOT/airuni_connect_flutter"
else
  FLUTTER_DIR="$REPO_ROOT"
fi

echo "[1/5] Flutter Project Path: $FLUTTER_DIR"
cd "$FLUTTER_DIR"

# 2. Fast Flutter SDK installation (Shallow single-branch clone)
FLUTTER_SDK_DIR="$HOME/flutter"
if [ ! -x "$FLUTTER_SDK_DIR/bin/flutter" ]; then
  echo "[2/5] Downloading Flutter SDK (shallow single-branch)..."
  git clone https://github.com/flutter/flutter.git --depth 1 --single-branch -b stable "$FLUTTER_SDK_DIR"
else
  echo "[2/5] Flutter SDK already cached at $FLUTTER_SDK_DIR"
fi

export PATH="$PATH:$FLUTTER_SDK_DIR/bin"
flutter --version

# 3. CocoaPods & network optimizations
echo "[3/5] Verifying CocoaPods availability..."
export HOMEBREW_NO_AUTO_UPDATE=1
export HOMEBREW_NO_INSTALL_CLEANUP=1
export COCOAPODS_DISABLE_STATS=true

if ! command -v pod >/dev/null 2>&1; then
  echo "Installing CocoaPods via Homebrew..."
  brew install cocoapods
else
  echo "CocoaPods is pre-installed on Xcode Cloud runner: $(pod --version)"
fi

# 4. Resolve Version & Build Number based on boolean flag
PLIST_PATH="$FLUTTER_DIR/ios/Runner/Info.plist"

if [ "$USE_INFO_PLIST_VERSION" = "true" ]; then
  echo "[4/5] Version Source Mode: Info.plist (USE_INFO_PLIST_VERSION=true)"
  VERSION_NAME=$(/usr/libexec/PlistBuddy -c "Print :CFBundleShortVersionString" "$PLIST_PATH" 2>/dev/null || echo "1.0.0")
  BUILD_NUMBER=$(/usr/libexec/PlistBuddy -c "Print :CFBundleVersion" "$PLIST_PATH" 2>/dev/null || echo "1")
  echo "Resolved from Info.plist: $VERSION_NAME (Build: $BUILD_NUMBER)"
else
  echo "[4/5] Version Source Mode: pubspec.yaml (USE_INFO_PLIST_VERSION=false)"
  VERSION_LINE=$(grep '^version:' pubspec.yaml | sed 's/version: //')
  VERSION_NAME=$(echo "$VERSION_LINE" | cut -d'+' -f1 | tr -d '[:space:]')
  PUBSPEC_BUILD=$(echo "$VERSION_LINE" | cut -d'+' -f2 | tr -d '[:space:]')

  # In Xcode Cloud, $CI_BUILD_NUMBER provides an auto-incrementing integer counter
  BUILD_NUMBER="${CI_BUILD_NUMBER:-$PUBSPEC_BUILD}"
  echo "Resolved from pubspec.yaml: $VERSION_NAME (Build: $BUILD_NUMBER)"

  # Synchronize Info.plist with pubspec.yaml values
  if [ -f "$PLIST_PATH" ]; then
    echo "Updating $PLIST_PATH with pubspec.yaml version..."
    /usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString $VERSION_NAME" "$PLIST_PATH" 2>/dev/null || \
    /usr/libexec/PlistBuddy -c "Add :CFBundleShortVersionString string $VERSION_NAME" "$PLIST_PATH"
    
    /usr/libexec/PlistBuddy -c "Set :CFBundleVersion $BUILD_NUMBER" "$PLIST_PATH" 2>/dev/null || \
    /usr/libexec/PlistBuddy -c "Add :CFBundleVersion string $BUILD_NUMBER" "$PLIST_PATH"
  fi
fi

# 5. Precache iOS artifacts, fetch pub packages, and generate Generated.xcconfig
echo "[5/5] Generating configurations and resolving dependencies..."
flutter precache --ios --no-android
flutter pub get

# Generate Generated.xcconfig and execute Pods integration in a single pass
flutter build ios --config-only --no-pub --build-name="$VERSION_NAME" --build-number="$BUILD_NUMBER"

echo "=========================================================="
echo "  Xcode Cloud Environment Configured Successfully!        "
echo "  Handing over to Xcode Cloud xcodebuild engine...        "
echo "=========================================================="

Step-by-Step Xcode Cloud Setup Walkthrough

Step 0: Initialize Git & Push to GitHub

Xcode Cloud requires your code and schemes to be pushed to your remote repository. If git is not yet configured:

Terminal — Git Push
git init
git add .
git commit -m "feat(ci): initial setup for Xcode Cloud"
git branch -M main
git remote add origin https://github.com/<your-org>/<your-repo>.git
git push -u origin main
Step 1: Save Script & Set Executable Permissions

Save the script above as airuni_connect_flutter/ios/ci_scripts/ci_post_clone.sh and grant execution permissions:

Terminal — Permissions
chmod +x airuni_connect_flutter/ios/ci_scripts/ci_post_clone.sh
chmod +x ci_scripts/ci_post_clone.sh

Ensure your .gitignore ignores local build caches while tracking ci_scripts:

.gitignore
*.log
xcodelogs
.dart_tool/
.pub-cache/
/build/
ios/Flutter/Generated.xcconfig
ios/Flutter/ephemeral/
Step 2: Connect to Xcode Cloud in Xcode

1. Open your project in Xcode: open airuni_connect_flutter/ios/Runner.xcworkspace.
(Note: Run pod install locally once before opening if you haven't already).
2. Open the Report Navigator by pressing Cmd + 9.
3. At the top toggle, select Cloud.
4. Tap Get Started.
5. Select App: Choose the iOS app target (the one showing your app icon and name).
6. Select Team: Choose your Apple Developer Team → tap Next.
7. Attach Repository: If repository is not connected, tap Connect with the repository. A browser opens to App Store Connect → complete authorization with the Apple Xcode Cloud GitHub App → select your GitHub organization/account and authorize.
8. Return to Xcode → tap Next → tap Close.

Step 3: Edit the Default Workflow

In the Cloud tab under Runner, you will see a default workflow (usually named Default).
1. Right-click on Default → select Edit Workflow...
2. General Tab: Rename workflow to Production App Store Release (or keep as is).
3. Environment Tab:
- macOS Version: Choose Latest Release (macOS Sonoma / Sequoia).
- Xcode Version: Choose Latest Release (Xcode 16.x or 15.x).
- Clean Build: Checked.
- Environment Variables: If you wish to use Info.plist for versioning, click + and set USE_INFO_PLIST_VERSION to true.

Step 4: Configure Branch Changes (Production Trigger)

1. In the left sidebar, click Branch Changes under Start Conditions.
2. Change from Any Branch to Custom Branch.
3. Click the + button → enter production.
4. Select the old default branch (e.g. main or master) and click the - button to remove it.
5. Under Files & Folders, select Start a build if any file changes.
6. Ensure Auto-cancel builds is checked.

Step 5: Configure the Archive Action

1. In the left sidebar, click Actions.
2. If empty, click +. If another action exists, delete it and click +.
3. Select Archive:
- Platform: iOS
- Scheme: Runner
- Deployment Preparation: Select App Store Connect *(or TestFlight Internal Testing Only)*.

Step 6: Configure Post-Actions & Save

1. In the left sidebar, click Post-Actions.
2. Click + to add automations:
- TestFlight (Internal Testing Only): Choose your internal testers group (e.g., Core Team) for instant deployment.
- Notify: Add email addresses or Slack webhooks for build notifications.
3. Click Save in the bottom right corner.

Triggering Your Release

Whenever you are ready to publish a new build, simply push or merge to production:

Terminal — Release
git checkout production
git merge main
git push origin production

Xcode Cloud will detect the push, execute ci_post_clone.sh to configure Flutter and CocoaPods in under 60 seconds, compile and sign the archive via Apple-managed signing, and distribute it directly to TestFlight and App Store Connect! 🚀

0 Comments

⚠️

Ad Blocker Detected

We noticed you are using an ad blocker. We rely on ads to keep this website free for everyone. Please disable your ad blocker or whitelist our site to continue.