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.
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:
#!/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
Xcode Cloud requires your code and schemes to be pushed to your remote repository. If git is not yet configured:
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
Save the script above as airuni_connect_flutter/ios/ci_scripts/ci_post_clone.sh and grant execution 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:
*.log
xcodelogs
.dart_tool/
.pub-cache/
/build/
ios/Flutter/Generated.xcconfig
ios/Flutter/ephemeral/
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.
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.
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.
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)*.
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:
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