iOS Build Automation Setup
Build Automation for iOS is not just adding xcodebuild to CI. It's managing certificates, provisioning profiles, schemes, configurations, build numbers, and artifacts so builds are reproducible identically on any machine without manual intervention.
iOS Build Anatomy and Failure Points
Typical build command:
xcodebuild archive \
-workspace MyApp.xcworkspace \
-scheme MyApp \
-configuration Release \
-destination generic/platform=iOS \
-archivePath build/MyApp.xcarchive \
CODE_SIGN_STYLE=Manual \
PROVISIONING_PROFILE_SPECIFIER="MyApp AdHoc" \
CODE_SIGN_IDENTITY="Apple Distribution: Acme Corp (XXXXXXXXXX)"
Four code signing parameters—each can fail independently. CODE_SIGN_IDENTITY requires exact certificate name from Keychain. PROVISIONING_PROFILE_SPECIFIER—exact profile name installed on machine. On fresh CI runner, neither exists.
Solution strategy: fastlane match or manual certificate + profile installation via script (described below).
Manual Certificate Installation Without Fastlane
If fastlane match doesn't fit:
#!/bin/bash
set -euo pipefail
# Create temporary keychain
KEYCHAIN_NAME="ci-build.keychain"
KEYCHAIN_PASS=$(openssl rand -hex 16)
security create-keychain -p "$KEYCHAIN_PASS" "$KEYCHAIN_NAME"
security set-keychain-settings -lut 7200 "$KEYCHAIN_NAME"
security unlock-keychain -p "$KEYCHAIN_PASS" "$KEYCHAIN_NAME"
security list-keychains -d user -s "$KEYCHAIN_NAME" login.keychain
# Import certificate
echo "$DISTRIBUTION_CERT_BASE64" | base64 --decode > /tmp/cert.p12
security import /tmp/cert.p12 -k "$KEYCHAIN_NAME" \
-P "$CERT_PASSWORD" -T /usr/bin/codesign -T /usr/bin/xcodebuild
security set-key-partition-list -S apple-tool:,apple: -s -k "$KEYCHAIN_PASS" "$KEYCHAIN_NAME"
# Install provisioning profile
PROFILE_PATH="$HOME/Library/MobileDevice/Provisioning Profiles"
mkdir -p "$PROFILE_PATH"
echo "$PROVISIONING_PROFILE_BASE64" | base64 --decode > "/tmp/profile.mobileprovision"
PROFILE_UUID=$(grep -a -A 1 'UUID' /tmp/profile.mobileprovision | grep string | sed 's/.*<string>//;s/<\/string>//')
cp /tmp/profile.mobileprovision "$PROFILE_PATH/$PROFILE_UUID.mobileprovision"
Script is parameterized via environment variables—all secrets come from CI, not stored in script.
xcconfig and Configuration Management
Hardcoding Bundle ID, Team ID, Provisioning Profile in .pbxproj—path to merge conflicts. Better—xcconfig files:
# Configurations/Release.xcconfig
PRODUCT_BUNDLE_IDENTIFIER = com.acme.myapp
DEVELOPMENT_TEAM = XXXXXXXXXX
PROVISIONING_PROFILE_SPECIFIER = MyApp AppStore
CODE_SIGN_IDENTITY = Apple Distribution
In Xcode: Project → Info → Configurations → specify xcconfig for each. On CI pass PROVISIONING_PROFILE_SPECIFIER via environment, don't rewrite xcconfig.
Build Number: Automation Without Collisions
# From CI pipeline number
BUILD_NUMBER=${CI_PIPELINE_IID:-$(git rev-list --count HEAD)}
/usr/libexec/PlistBuddy -c "Set :CFBundleVersion $BUILD_NUMBER" MyApp/Info.plist
# Or via agvtool
xcrun agvtool new-version -all $BUILD_NUMBER
agvtool updates CFBundleVersion in all project Info.plist files, including Extensions—important for Watch, Notification Service, Share Extensions.
Export and Artifacts
After archiving—export .ipa:
xcodebuild -exportArchive \
-archivePath build/MyApp.xcarchive \
-exportPath build/export \
-exportOptionsPlist ExportOptions.plist
ExportOptions.plist—critical file:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "...">
<plist version="1.0">
<dict>
<key>method</key>
<string>ad-hoc</string>
<key>teamID</key>
<string>XXXXXXXXXX</string>
<key>provisioningProfiles</key>
<dict>
<key>com.acme.myapp</key>
<string>MyApp AdHoc</string>
</dict>
<key>uploadBitcode</key>
<false/>
<key>thinning</key>
<string><none></string>
</dict>
</plist>
For different targets (main + Extensions)—add all bundle IDs to provisioningProfiles.
Building Without Xcode: xcode-build-server + LSP
For teams with CI without Xcode license limits: xcode-build-server allows using xcodebuild from command line without opening Xcode. Useful on headless Mac mini with minimal GUI.
Timeline
Basic build script with signing (without fastlane): 3–5 days. Full automation with xcconfig, automatic build number, multiple targets, CI integration: 1–2 weeks. Cost calculated individually.







