
Ci Cd Setup
- 2 installs
- 591 repo stars
- Updated July 24, 2026
- rshankras/claude-code-apple-skills
Generates CI/CD configuration for automated builds, tests, and distribution of iOS/macOS apps using GitHub Actions, Xcode Cloud, or fastlane.
About
Generates continuous integration and deployment config for iOS/macOS apps across GitHub Actions, Xcode Cloud, and fastlane. A developer uses it to automate builds, tests, and TestFlight or App Store delivery.
- Supports GitHub Actions, Xcode Cloud, and fastlane
- Checks for existing CI config before generating
Ci Cd Setup by the numbers
- 2 all-time installs (skills.sh)
- Ranked #1,138 of 1,435 DevOps & CI/CD skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/rshankras/claude-code-apple-skills --skill ci-cd-setupAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 2 |
|---|---|
| repo stars | ★ 591 |
| Last updated | July 24, 2026 |
| Repository | rshankras/claude-code-apple-skills ↗ |
What it does
Generates CI/CD configuration for automated builds, tests, and distribution of iOS/macOS apps using GitHub Actions, Xcode Cloud, or fastlane.
Files
CI/CD Setup Generator
Generate CI/CD configuration for automated builds, tests, and distribution of iOS/macOS apps.
When This Skill Activates
- User wants to automate their build and test process
- User mentions GitHub Actions, Xcode Cloud, or fastlane
- User wants to set up TestFlight or App Store deployment
- User asks about continuous integration for their app
Pre-Generation Checks
Before generating, verify:
1. Existing CI Configuration
# Check for existing CI files
ls -la .github/workflows/ 2>/dev/null
ls -la ci_scripts/ 2>/dev/null
ls -la fastlane/ 2>/dev/null2. Project Structure
# Find Xcode project/workspace
find . -name "*.xcodeproj" -o -name "*.xcworkspace" | head -53. Package Manager
# Check for SPM vs CocoaPods
ls Package.swift 2>/dev/null
ls Podfile 2>/dev/nullConfiguration Questions
1. CI/CD Platform
- GitHub Actions (Recommended) - Full control, extensive marketplace
- Xcode Cloud - Native Apple integration, simpler setup
- Both - GitHub for PRs/tests, Xcode Cloud for releases
2. Distribution Method
- TestFlight - Beta testing via App Store Connect
- App Store - Production releases
- Direct (macOS only) - Notarized DMG/PKG distribution
- All - Full pipeline from dev to production
3. Include fastlane?
- Yes - Advanced automation, match for code signing
- No - Simpler setup using xcodebuild directly
4. Code Signing Approach
- Manual - Certificates in GitHub Secrets
- match (fastlane) - Git-based certificate management
- Xcode Cloud Managed - Apple handles signing
Generated Files
GitHub Actions
.github/workflows/
├── build-test.yml # PR checks, unit tests
├── deploy-testflight.yml # TestFlight deployment
└── deploy-appstore.yml # App Store submissionXcode Cloud
ci_scripts/
├── ci_post_clone.sh # Post-clone setup
└── ci_pre_xcodebuild.sh # Pre-build configurationfastlane
fastlane/
├── Fastfile # Lane definitions
├── Appfile # App configuration
└── Matchfile # Code signing (if using match)Integration Steps
GitHub Actions Setup
1. Add Repository Secrets (Settings > Secrets and variables > Actions):
APP_STORE_CONNECT_API_KEY_ID- API Key IDAPP_STORE_CONNECT_API_ISSUER_ID- Issuer IDAPP_STORE_CONNECT_API_KEY_CONTENT- Private key (.p8 content)CERTIFICATE_P12- Base64-encoded .p12 certificateCERTIFICATE_PASSWORD- Certificate passwordPROVISIONING_PROFILE- Base64-encoded provisioning profile
2. Create App Store Connect API Key:
- Go to App Store Connect > Users and Access > Keys
- Generate API Key with "App Manager" role
- Download the .p8 file (only available once)
3. Export Certificate:
# Export from Keychain as .p12, then base64 encode
base64 -i certificate.p12 | pbcopyXcode Cloud Setup
1. Enable Xcode Cloud in Xcode:
- Product > Xcode Cloud > Create Workflow
- Connect to App Store Connect
2. Configure Workflow:
- Set start conditions (branch, PR, tag)
- Configure environment variables
- Set up post-actions (TestFlight, App Store)
3. Add ci_scripts to repository for customization
fastlane Setup
1. Install fastlane:
brew install fastlane2. Initialize (if starting fresh):
fastlane init3. Set up match (optional, for code signing):
fastlane match init
fastlane match development
fastlane match appstoreBest Practices
Caching
- Cache Swift Package Manager dependencies
- Cache DerivedData for faster builds
- Use selective caching to avoid stale artifacts
Secrets Management
- Never commit certificates or keys
- Use environment variables for sensitive data
- Rotate API keys periodically
Build Optimization
- Use incremental builds where possible
- Parallelize test execution
- Skip unnecessary steps on draft PRs
Notifications
- Slack/Discord integration for build status
- Email notifications for failures
- GitHub status checks for PRs
References
CI/CD Patterns
Best practices for continuous integration and deployment of iOS/macOS apps.
GitHub Actions
Runner Selection
# macOS runners for Xcode builds
runs-on: macos-14 # M1 runner with Xcode 15+
runs-on: macos-15 # Latest with Xcode 16+
# Available Xcode versions (check runner images for current list)
# https://github.com/actions/runner-images/blob/main/images/macosXcode Version Selection
- name: Select Xcode
run: sudo xcode-select -s /Applications/Xcode_16.2.app/Contents/Developer
# Or use xcodes action for flexibility
- uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: '16.2'Caching Strategies
# Swift Package Manager cache
- name: Cache SPM
uses: actions/cache@v4
with:
path: |
.build
~/Library/Caches/org.swift.swiftpm
key: ${{ runner.os }}-spm-${{ hashFiles('**/Package.resolved') }}
restore-keys: |
${{ runner.os }}-spm-
# DerivedData cache (use with caution - can cause stale builds)
- name: Cache DerivedData
uses: actions/cache@v4
with:
path: ~/Library/Developer/Xcode/DerivedData
key: ${{ runner.os }}-derived-${{ hashFiles('**/*.xcodeproj/project.pbxproj') }}Code Signing
# Import certificate and provisioning profile
- name: Install Certificates
env:
CERTIFICATE_P12: ${{ secrets.CERTIFICATE_P12 }}
CERTIFICATE_PASSWORD: ${{ secrets.CERTIFICATE_PASSWORD }}
PROVISIONING_PROFILE: ${{ secrets.PROVISIONING_PROFILE }}
run: |
# Create temporary keychain
KEYCHAIN_PATH=$RUNNER_TEMP/signing.keychain-db
KEYCHAIN_PASSWORD=$(openssl rand -base64 32)
security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
# Import certificate
echo "$CERTIFICATE_P12" | base64 --decode > $RUNNER_TEMP/certificate.p12
security import $RUNNER_TEMP/certificate.p12 \
-P "$CERTIFICATE_PASSWORD" \
-A -t cert -f pkcs12 \
-k $KEYCHAIN_PATH
security set-key-partition-list -S apple-tool:,apple: \
-k "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security list-keychain -d user -s $KEYCHAIN_PATH
# Install provisioning profile
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
echo "$PROVISIONING_PROFILE" | base64 --decode \
> ~/Library/MobileDevice/Provisioning\ Profiles/profile.mobileprovision
- name: Cleanup Keychain
if: always()
run: |
security delete-keychain $RUNNER_TEMP/signing.keychain-db || trueApp Store Connect API
- name: Upload to TestFlight
env:
API_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}
API_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_API_ISSUER_ID }}
API_KEY_CONTENT: ${{ secrets.APP_STORE_CONNECT_API_KEY_CONTENT }}
run: |
# Create API key file
mkdir -p ~/.private_keys
echo "$API_KEY_CONTENT" > ~/.private_keys/AuthKey_$API_KEY_ID.p8
# Upload using xcrun altool or notarytool
xcrun altool --upload-app \
--type ios \
--file "$IPA_PATH" \
--apiKey "$API_KEY_ID" \
--apiIssuer "$API_ISSUER_ID"Xcode Cloud
Workflow Configuration
Xcode Cloud workflows are configured in App Store Connect or Xcode, but ci_scripts allow customization.
ci_scripts Lifecycle
1. ci_post_clone.sh - After repository clone
2. ci_pre_xcodebuild.sh - Before xcodebuild runs
3. ci_post_xcodebuild.sh - After successful buildEnvironment Variables
# Available in ci_scripts
$CI # Always "TRUE" in Xcode Cloud
$CI_WORKSPACE # Path to workspace
$CI_PRODUCT # Product name
$CI_XCODE_PROJECT # Project path
$CI_XCODE_SCHEME # Scheme name
$CI_BRANCH # Git branch
$CI_TAG # Git tag (if triggered by tag)
$CI_COMMIT # Commit SHA
$CI_BUILD_NUMBER # Xcode Cloud build number
$CI_BUNDLE_ID # Bundle identifier
$CI_TEAM_ID # Apple Developer Team IDCustom Environment Variables
Set in App Store Connect > Xcode Cloud > Workflow > Environment:
API_BASE_URL- Environment-specific URLsFEATURE_FLAGS- Build-time feature togglesSENTRY_DSN- Error monitoring (as secret)
fastlane
Lane Organization
# Fastfile structure
default_platform(:ios)
platform :ios do
# Shared setup
before_all do
setup_ci if is_ci
end
# Development
lane :test do
run_tests(scheme: "MyApp")
end
# Beta distribution
lane :beta do
increment_build_number
build_app(scheme: "MyApp")
upload_to_testflight
end
# Production release
lane :release do
increment_build_number
build_app(scheme: "MyApp")
upload_to_app_store(
submit_for_review: true,
automatic_release: false
)
end
# Error handling
error do |lane, exception|
# Notify on failure (Slack, etc.)
end
endCode Signing with match
# Matchfile
git_url("git@github.com:yourorg/certificates.git")
storage_mode("git")
type("appstore") # development, adhoc, appstore
app_identifier(["com.yourcompany.app"])
username("your@email.com")
# In Fastfile
lane :sync_certificates do
match(type: "development")
match(type: "appstore")
endBuild Actions
# Build for testing
lane :build_for_testing do
build_app(
scheme: "MyApp",
configuration: "Debug",
build_for_testing: true,
derived_data_path: "build/DerivedData"
)
end
# Build for release
lane :build_release do
build_app(
scheme: "MyApp",
configuration: "Release",
export_method: "app-store",
output_directory: "build",
output_name: "MyApp.ipa"
)
endVersioning
# Increment version
lane :bump_version do |options|
increment_version_number(
bump_type: options[:type] || "patch" # major, minor, patch
)
end
# Increment build number
lane :bump_build do
increment_build_number(
build_number: latest_testflight_build_number + 1
)
endmacOS Notarization
Using notarytool
# Submit for notarization
xcrun notarytool submit MyApp.dmg \
--apple-id "your@email.com" \
--team-id "TEAM_ID" \
--password "$APP_SPECIFIC_PASSWORD" \
--wait
# Staple the notarization ticket
xcrun stapler staple MyApp.dmgIn GitHub Actions
- name: Notarize App
env:
APPLE_ID: ${{ secrets.APPLE_ID }}
TEAM_ID: ${{ secrets.TEAM_ID }}
APP_PASSWORD: ${{ secrets.APP_SPECIFIC_PASSWORD }}
run: |
xcrun notarytool submit build/MyApp.dmg \
--apple-id "$APPLE_ID" \
--team-id "$TEAM_ID" \
--password "$APP_PASSWORD" \
--wait
xcrun stapler staple build/MyApp.dmgWith fastlane
lane :notarize do
notarize(
package: "build/MyApp.dmg",
bundle_id: "com.yourcompany.app",
username: ENV["APPLE_ID"],
asc_provider: ENV["TEAM_ID"]
)
endTesting Strategies
Unit Tests
- name: Run Unit Tests
run: |
xcodebuild test \
-scheme MyApp \
-destination 'platform=iOS Simulator,name=iPhone 16' \
-resultBundlePath TestResults.xcresult \
| xcbeautifyUI Tests
- name: Run UI Tests
run: |
xcodebuild test \
-scheme MyAppUITests \
-destination 'platform=iOS Simulator,name=iPhone 16' \
-testPlan UITests \
| xcbeautifyParallel Testing
- name: Run Tests in Parallel
run: |
xcodebuild test \
-scheme MyApp \
-destination 'platform=iOS Simulator,name=iPhone 16' \
-parallel-testing-enabled YES \
-parallel-testing-worker-count 4Build Matrix
Multiple Platform Builds
strategy:
matrix:
include:
- platform: iOS
destination: 'platform=iOS Simulator,name=iPhone 16'
- platform: macOS
destination: 'platform=macOS'
- platform: watchOS
destination: 'platform=watchOS Simulator,name=Apple Watch Series 10'
steps:
- name: Build for ${{ matrix.platform }}
run: |
xcodebuild build \
-scheme MyApp \
-destination '${{ matrix.destination }}'Multiple Xcode Versions
strategy:
matrix:
xcode: ['15.4', '16.0', '16.2']
steps:
- uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: ${{ matrix.xcode }}Workflow Triggers
Branch-Based
on:
push:
branches: [main, develop]
pull_request:
branches: [main]Tag-Based Releases
on:
push:
tags:
- 'v*.*.*'Manual Triggers
on:
workflow_dispatch:
inputs:
environment:
description: 'Deployment environment'
required: true
default: 'staging'
type: choice
options:
- staging
- productionNotifications
Slack Integration
- name: Notify Slack
if: failure()
uses: slackapi/slack-github-action@v1
with:
payload: |
{
"text": "Build failed for ${{ github.repository }}",
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*Build Failed* :x:\n*Repo:* ${{ github.repository }}\n*Branch:* ${{ github.ref_name }}"
}
}
]
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}GitHub Status Checks
- name: Update Commit Status
uses: actions/github-script@v7
with:
script: |
github.rest.repos.createCommitStatus({
owner: context.repo.owner,
repo: context.repo.repo,
sha: context.sha,
state: 'success',
description: 'Build passed',
context: 'CI/Build'
})# Appfile
# App-specific configuration for fastlane
#
# Documentation: https://docs.fastlane.tools/advanced/Appfile/
# ============================================
# App Identifiers
# ============================================
# The bundle identifier of your app
app_identifier("com.yourcompany.yourapp") # TODO: Replace with your bundle ID
# Your Apple Developer Portal username
apple_id("your@email.com") # TODO: Replace with your Apple ID
# ============================================
# App Store Connect
# ============================================
# For App Store Connect API authentication (recommended)
# Generate key at: https://appstoreconnect.apple.com/access/api
#
# Set these as environment variables:
# - APP_STORE_CONNECT_API_KEY_ID
# - APP_STORE_CONNECT_API_ISSUER_ID
# - APP_STORE_CONNECT_API_KEY_PATH (or APP_STORE_CONNECT_API_KEY_CONTENT)
# Uncomment to use API key authentication:
# app_store_connect_api_key(
# key_id: ENV["APP_STORE_CONNECT_API_KEY_ID"],
# issuer_id: ENV["APP_STORE_CONNECT_API_ISSUER_ID"],
# key_filepath: ENV["APP_STORE_CONNECT_API_KEY_PATH"],
# # Or use key content directly:
# # key_content: ENV["APP_STORE_CONNECT_API_KEY_CONTENT"],
# duration: 1200, # optional
# in_house: false # true for Enterprise accounts
# )
# ============================================
# Team Configuration
# ============================================
# Developer Portal Team ID
team_id("YOUR_TEAM_ID") # TODO: Replace with your Team ID
# App Store Connect Team ID (if different)
# itc_team_id("YOUR_ITC_TEAM_ID")
# ============================================
# Lane-specific Configuration
# ============================================
# Different configurations for different lanes
# for_lane :beta do
# app_identifier("com.yourcompany.yourapp.beta")
# end
# for_lane :release do
# app_identifier("com.yourcompany.yourapp")
# end
# ============================================
# Platform-specific Configuration
# ============================================
# for_platform :mac do
# app_identifier("com.yourcompany.yourapp.mac")
# end
# Fastfile
# Automated build and deployment lanes for iOS/macOS apps
#
# Installation:
# brew install fastlane
#
# Usage:
# fastlane test # Run tests
# fastlane beta # Deploy to TestFlight
# fastlane release # Deploy to App Store
# fastlane build_macos # Build macOS app
#
# Documentation: https://docs.fastlane.tools
default_platform(:ios)
# ============================================
# iOS Platform
# ============================================
platform :ios do
# ----------------------------------------
# Before all lanes
# ----------------------------------------
before_all do
# Setup CI environment if running on CI
setup_ci if is_ci
# Ensure we're on a clean git state (optional)
# ensure_git_status_clean
end
# ----------------------------------------
# Testing
# ----------------------------------------
desc "Run all tests"
lane :test do
run_tests(
scheme: ENV["SCHEME"] || "YourApp", # TODO: Replace with your scheme
device: "iPhone 16",
clean: true,
code_coverage: true,
result_bundle: true
)
end
desc "Run tests and generate coverage report"
lane :test_coverage do
test
# Generate coverage report (requires slather)
# slather(
# scheme: ENV["SCHEME"] || "YourApp",
# proj: "YourApp.xcodeproj",
# html: true,
# output_directory: "coverage"
# )
end
# ----------------------------------------
# Code Signing
# ----------------------------------------
desc "Sync certificates and profiles using match"
lane :sync_certificates do
match(type: "development")
match(type: "appstore")
end
desc "Sync development certificates"
lane :sync_dev_certs do
match(type: "development", readonly: is_ci)
end
desc "Sync distribution certificates"
lane :sync_dist_certs do
match(type: "appstore", readonly: is_ci)
end
# ----------------------------------------
# Building
# ----------------------------------------
desc "Build for testing"
lane :build do
build_app(
scheme: ENV["SCHEME"] || "YourApp", # TODO: Replace with your scheme
configuration: "Debug",
skip_archive: true,
skip_codesigning: true
)
end
desc "Build release archive"
lane :build_release do
# Sync certificates first
sync_dist_certs if is_ci
build_app(
scheme: ENV["SCHEME"] || "YourApp", # TODO: Replace with your scheme
configuration: "Release",
export_method: "app-store",
output_directory: "build",
output_name: "YourApp.ipa" # TODO: Replace with your app name
)
end
# ----------------------------------------
# TestFlight Deployment
# ----------------------------------------
desc "Deploy to TestFlight"
lane :beta do
# Increment build number
increment_build_number(
build_number: latest_testflight_build_number + 1
)
# Build
build_release
# Upload to TestFlight
upload_to_testflight(
skip_waiting_for_build_processing: true,
distribute_external: false
)
# Commit version bump (optional)
# commit_version_bump(
# message: "Bump build number [skip ci]"
# )
end
desc "Deploy to TestFlight with external testers"
lane :beta_external do
beta
# Distribute to external testers
upload_to_testflight(
distribute_external: true,
groups: ["Beta Testers"], # TODO: Set your tester group name
changelog: "Bug fixes and improvements"
)
end
# ----------------------------------------
# App Store Deployment
# ----------------------------------------
desc "Deploy to App Store"
lane :release do |options|
# Bump version if specified
if options[:bump]
increment_version_number(bump_type: options[:bump])
end
# Run tests first
test
# Build release
build_release
# Upload to App Store
upload_to_app_store(
submit_for_review: options[:submit] || false,
automatic_release: false,
skip_screenshots: true,
skip_metadata: true,
precheck_include_in_app_purchases: false
)
end
desc "Deploy to App Store and submit for review"
lane :release_and_submit do
release(submit: true)
end
# ----------------------------------------
# Versioning
# ----------------------------------------
desc "Bump version number"
lane :bump do |options|
bump_type = options[:type] || "patch"
increment_version_number(bump_type: bump_type)
version = get_version_number
UI.success("Version bumped to #{version}")
end
desc "Bump build number"
lane :bump_build do
increment_build_number
build = get_build_number
UI.success("Build number bumped to #{build}")
end
# ----------------------------------------
# Utilities
# ----------------------------------------
desc "Generate app icons"
lane :icons do
# Requires appicon plugin: fastlane add_plugin appicon
# appicon(
# appicon_image_file: "fastlane/metadata/app_icon.png",
# appicon_devices: [:iphone, :ipad],
# appicon_path: "YourApp/Assets.xcassets"
# )
UI.important("Icon generation requires appicon plugin")
end
desc "Take screenshots"
lane :screenshots do
capture_screenshots(
scheme: ENV["SCHEME"] || "YourAppUITests",
devices: [
"iPhone 16 Pro Max",
"iPhone 16",
"iPad Pro (12.9-inch)"
],
languages: ["en-US"],
clear_previous_screenshots: true
)
end
# ----------------------------------------
# Error handling
# ----------------------------------------
error do |lane, exception|
# Notify on error (configure as needed)
# slack(
# message: "Error in lane #{lane}: #{exception.message}",
# success: false
# )
UI.error("Lane #{lane} failed: #{exception.message}")
end
after_all do |lane|
UI.success("Successfully completed #{lane}")
end
end
# ============================================
# macOS Platform
# ============================================
platform :mac do
desc "Build macOS app"
lane :build do
build_mac_app(
scheme: ENV["SCHEME"] || "YourApp", # TODO: Replace with your scheme
configuration: "Release",
export_method: "developer-id", # or "app-store" for Mac App Store
output_directory: "build"
)
end
desc "Build and notarize macOS app"
lane :release do
build
# Notarize the app
notarize(
package: "build/YourApp.app", # TODO: Replace with your app name
bundle_id: ENV["BUNDLE_ID"] || "com.yourcompany.yourapp",
username: ENV["APPLE_ID"],
asc_provider: ENV["TEAM_ID"],
print_log: true,
verbose: true
)
end
desc "Build for Mac App Store"
lane :appstore do
build_mac_app(
scheme: ENV["SCHEME"] || "YourApp",
configuration: "Release",
export_method: "app-store",
output_directory: "build"
)
upload_to_app_store(
platform: "osx",
skip_screenshots: true,
skip_metadata: true
)
end
end
# Matchfile
# Configuration for fastlane match (code signing)
#
# match creates and maintains certificates and provisioning profiles
# stored in a private git repository or cloud storage.
#
# Setup:
# fastlane match init
# fastlane match development
# fastlane match appstore
#
# Documentation: https://docs.fastlane.tools/actions/match/
# ============================================
# Storage Configuration
# ============================================
# Git repository for certificates (most common)
storage_mode("git")
git_url("git@github.com:yourorg/certificates.git") # TODO: Private repo for certs
# Alternative: Google Cloud Storage
# storage_mode("google_cloud")
# google_cloud_bucket_name("your-bucket-name")
# Alternative: Amazon S3
# storage_mode("s3")
# s3_bucket("your-bucket-name")
# s3_region("us-east-1")
# ============================================
# Certificate Types
# ============================================
# Default type (can be overridden per-run)
type("appstore") # development, adhoc, appstore, enterprise
# ============================================
# App Identifiers
# ============================================
# App identifiers to create profiles for
app_identifier([
"com.yourcompany.yourapp", # TODO: Main app
# "com.yourcompany.yourapp.widget", # Widget extension
# "com.yourcompany.yourapp.watchapp" # Watch app
])
# ============================================
# Team Configuration
# ============================================
# Your Apple Developer Team ID
team_id("YOUR_TEAM_ID") # TODO: Replace with your Team ID
# Apple ID for creating certificates
username("your@email.com") # TODO: Replace with your Apple ID
# ============================================
# Options
# ============================================
# Don't generate new certificates/profiles, just download existing
# readonly(true) # Enable on CI to prevent accidental regeneration
# Platform
# platform("ios") # ios, macos, catalyst
# Skip confirmation prompts
# force(true)
# ============================================
# Git Options (for git storage mode)
# ============================================
# Branch to use for certificates
git_branch("main")
# Clone depth (set to 1 for faster clones)
# shallow_clone(true)
# ============================================
# Keychain (for CI)
# ============================================
# Custom keychain name (useful for CI)
# keychain_name("fastlane_keychain")
# keychain_password(ENV["MATCH_KEYCHAIN_PASSWORD"])
# ============================================
# Additional Certificates
# ============================================
# Include additional certificate types
# additional_cert_types(["mac_installer_distribution"])
# Build and Test Workflow
# Runs on pull requests and pushes to main branch
# Tests on iOS Simulator and optionally macOS
name: Build and Test
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
# Cancel in-progress runs for the same branch
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
SCHEME: "YOUR_SCHEME_NAME" # TODO: Replace with your scheme name
DEVELOPER_DIR: /Applications/Xcode_16.2.app/Contents/Developer
jobs:
build-and-test:
name: Build and Test
runs-on: macos-15
timeout-minutes: 30
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Select Xcode
run: sudo xcode-select -s $DEVELOPER_DIR
- name: Show Xcode Version
run: xcodebuild -version
- name: Cache Swift Packages
uses: actions/cache@v4
with:
path: |
.build
~/Library/Caches/org.swift.swiftpm
~/Library/Developer/Xcode/DerivedData/**/SourcePackages
key: ${{ runner.os }}-spm-${{ hashFiles('**/Package.resolved') }}
restore-keys: |
${{ runner.os }}-spm-
- name: Resolve Package Dependencies
run: |
xcodebuild -resolvePackageDependencies \
-scheme "$SCHEME" \
-clonedSourcePackagesDirPath SourcePackages
- name: Build for Testing (iOS)
run: |
xcodebuild build-for-testing \
-scheme "$SCHEME" \
-destination 'platform=iOS Simulator,name=iPhone 16' \
-derivedDataPath build/DerivedData \
-clonedSourcePackagesDirPath SourcePackages \
CODE_SIGNING_ALLOWED=NO \
| xcbeautify --renderer github-actions
- name: Run Tests (iOS)
run: |
xcodebuild test-without-building \
-scheme "$SCHEME" \
-destination 'platform=iOS Simulator,name=iPhone 16' \
-derivedDataPath build/DerivedData \
-resultBundlePath build/TestResults.xcresult \
| xcbeautify --renderer github-actions
- name: Upload Test Results
uses: actions/upload-artifact@v4
if: always()
with:
name: test-results
path: build/TestResults.xcresult
retention-days: 7
# Optional: macOS build
build-macos:
name: Build (macOS)
runs-on: macos-15
timeout-minutes: 20
# Uncomment to enable macOS builds
# if: false
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Select Xcode
run: sudo xcode-select -s $DEVELOPER_DIR
- name: Cache Swift Packages
uses: actions/cache@v4
with:
path: |
.build
~/Library/Caches/org.swift.swiftpm
key: ${{ runner.os }}-spm-${{ hashFiles('**/Package.resolved') }}
restore-keys: |
${{ runner.os }}-spm-
- name: Build (macOS)
run: |
xcodebuild build \
-scheme "$SCHEME" \
-destination 'platform=macOS' \
CODE_SIGNING_ALLOWED=NO \
| xcbeautify --renderer github-actions
# SwiftLint (optional)
lint:
name: SwiftLint
runs-on: macos-15
timeout-minutes: 5
# Uncomment to enable linting
# if: false
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Run SwiftLint
run: |
if command -v swiftlint &> /dev/null; then
swiftlint lint --reporter github-actions-logging
else
echo "SwiftLint not installed, skipping"
fi
# App Store Deployment Workflow
# Builds and submits to App Store on version tag push
name: Deploy to App Store
on:
push:
tags:
- 'v*.*.*'
workflow_dispatch:
inputs:
submit_for_review:
description: 'Submit for App Review'
required: false
default: true
type: boolean
# Only one production deployment at a time
concurrency:
group: appstore-deployment
cancel-in-progress: false
env:
SCHEME: "YOUR_SCHEME_NAME" # TODO: Replace with your scheme name
PRODUCT_NAME: "YourApp" # TODO: Replace with your product name
DEVELOPER_DIR: /Applications/Xcode_16.2.app/Contents/Developer
jobs:
deploy:
name: Build and Submit
runs-on: macos-15
timeout-minutes: 60
environment: production # Requires approval if configured
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Get Version from Tag
id: version
run: |
if [[ "$GITHUB_REF" == refs/tags/v* ]]; then
VERSION=${GITHUB_REF#refs/tags/v}
echo "VERSION=$VERSION" >> $GITHUB_OUTPUT
else
VERSION=$(date +%Y.%m.%d)
echo "VERSION=$VERSION" >> $GITHUB_OUTPUT
fi
echo "Building version: $VERSION"
- name: Select Xcode
run: sudo xcode-select -s $DEVELOPER_DIR
- name: Cache Swift Packages
uses: actions/cache@v4
with:
path: |
.build
~/Library/Caches/org.swift.swiftpm
key: ${{ runner.os }}-spm-${{ hashFiles('**/Package.resolved') }}
restore-keys: |
${{ runner.os }}-spm-
# Install signing certificate
- name: Install Certificate
env:
CERTIFICATE_P12: ${{ secrets.CERTIFICATE_P12 }}
CERTIFICATE_PASSWORD: ${{ secrets.CERTIFICATE_PASSWORD }}
run: |
KEYCHAIN_PATH=$RUNNER_TEMP/signing.keychain-db
KEYCHAIN_PASSWORD=$(openssl rand -base64 32)
security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
echo "$CERTIFICATE_P12" | base64 --decode > $RUNNER_TEMP/certificate.p12
security import $RUNNER_TEMP/certificate.p12 \
-P "$CERTIFICATE_PASSWORD" \
-A -t cert -f pkcs12 \
-k $KEYCHAIN_PATH
security set-key-partition-list -S apple-tool:,apple: \
-k "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security list-keychain -d user -s $KEYCHAIN_PATH
echo "KEYCHAIN_PATH=$KEYCHAIN_PATH" >> $GITHUB_ENV
- name: Install Provisioning Profile
env:
PROVISIONING_PROFILE: ${{ secrets.PROVISIONING_PROFILE }}
run: |
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
echo "$PROVISIONING_PROFILE" | base64 --decode \
> ~/Library/MobileDevice/Provisioning\ Profiles/distribution.mobileprovision
- name: Setup App Store Connect API
env:
API_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}
API_KEY_CONTENT: ${{ secrets.APP_STORE_CONNECT_API_KEY_CONTENT }}
run: |
mkdir -p ~/.private_keys
echo "$API_KEY_CONTENT" > ~/.private_keys/AuthKey_$API_KEY_ID.p8
# Run tests before release
- name: Run Tests
run: |
xcodebuild test \
-scheme "$SCHEME" \
-destination 'platform=iOS Simulator,name=iPhone 16' \
CODE_SIGNING_ALLOWED=NO \
| xcbeautify --renderer github-actions
# Build archive
- name: Build Archive
run: |
xcodebuild archive \
-scheme "$SCHEME" \
-archivePath build/$PRODUCT_NAME.xcarchive \
-destination 'generic/platform=iOS' \
CODE_SIGN_STYLE=Manual \
MARKETING_VERSION=${{ steps.version.outputs.VERSION }} \
| xcbeautify --renderer github-actions
# Create ExportOptions.plist
- name: Create Export Options
run: |
cat > build/ExportOptions.plist << EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>method</key>
<string>app-store</string>
<key>destination</key>
<string>upload</string>
<key>signingStyle</key>
<string>manual</string>
<key>provisioningProfiles</key>
<dict>
<key>YOUR_BUNDLE_ID</key>
<string>YOUR_PROFILE_NAME</string>
</dict>
</dict>
</plist>
EOF
# Export IPA
- name: Export IPA
run: |
xcodebuild -exportArchive \
-archivePath build/$PRODUCT_NAME.xcarchive \
-exportPath build/Export \
-exportOptionsPlist build/ExportOptions.plist \
| xcbeautify --renderer github-actions
# Upload to App Store Connect
- name: Upload to App Store Connect
env:
API_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}
API_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_API_ISSUER_ID }}
run: |
xcrun altool --upload-app \
--type ios \
--file "build/Export/$PRODUCT_NAME.ipa" \
--apiKey "$API_KEY_ID" \
--apiIssuer "$API_ISSUER_ID"
# Create GitHub Release
- name: Create Release
if: startsWith(github.ref, 'refs/tags/')
uses: softprops/action-gh-release@v2
with:
files: |
build/Export/${{ env.PRODUCT_NAME }}.ipa
generate_release_notes: true
draft: false
# Cleanup
- name: Cleanup Keychain
if: always()
run: |
security delete-keychain $KEYCHAIN_PATH || true
# Upload artifacts
- name: Upload Archive
uses: actions/upload-artifact@v4
with:
name: release-archive
path: build/${{ env.PRODUCT_NAME }}.xcarchive
retention-days: 90
- name: Upload IPA
uses: actions/upload-artifact@v4
with:
name: release-ipa
path: build/Export/${{ env.PRODUCT_NAME }}.ipa
retention-days: 90
# Notify on completion
notify:
name: Notify
runs-on: ubuntu-latest
needs: deploy
if: always()
steps:
- name: Notify Success
if: needs.deploy.result == 'success'
run: |
echo "App Store submission successful!"
# Add Slack/Discord notification here
- name: Notify Failure
if: needs.deploy.result == 'failure'
run: |
echo "App Store submission failed!"
# Add Slack/Discord notification here
# TestFlight Deployment Workflow
# Builds and uploads to TestFlight on push to main or manual trigger
name: Deploy to TestFlight
on:
push:
branches: [main]
paths-ignore:
- '**.md'
- '.github/workflows/build-test.yml'
workflow_dispatch:
inputs:
bump_build:
description: 'Increment build number'
required: false
default: true
type: boolean
# Only one deployment at a time
concurrency:
group: testflight-deployment
cancel-in-progress: false
env:
SCHEME: "YOUR_SCHEME_NAME" # TODO: Replace with your scheme name
PRODUCT_NAME: "YourApp" # TODO: Replace with your product name
DEVELOPER_DIR: /Applications/Xcode_16.2.app/Contents/Developer
jobs:
deploy:
name: Build and Deploy
runs-on: macos-15
timeout-minutes: 45
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0 # Full history for build number
- name: Select Xcode
run: sudo xcode-select -s $DEVELOPER_DIR
- name: Cache Swift Packages
uses: actions/cache@v4
with:
path: |
.build
~/Library/Caches/org.swift.swiftpm
key: ${{ runner.os }}-spm-${{ hashFiles('**/Package.resolved') }}
restore-keys: |
${{ runner.os }}-spm-
# Install signing certificate
- name: Install Certificate
env:
CERTIFICATE_P12: ${{ secrets.CERTIFICATE_P12 }}
CERTIFICATE_PASSWORD: ${{ secrets.CERTIFICATE_PASSWORD }}
run: |
# Create temporary keychain
KEYCHAIN_PATH=$RUNNER_TEMP/signing.keychain-db
KEYCHAIN_PASSWORD=$(openssl rand -base64 32)
security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
# Import certificate
echo "$CERTIFICATE_P12" | base64 --decode > $RUNNER_TEMP/certificate.p12
security import $RUNNER_TEMP/certificate.p12 \
-P "$CERTIFICATE_PASSWORD" \
-A -t cert -f pkcs12 \
-k $KEYCHAIN_PATH
security set-key-partition-list -S apple-tool:,apple: \
-k "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security list-keychain -d user -s $KEYCHAIN_PATH
# Save keychain path for cleanup
echo "KEYCHAIN_PATH=$KEYCHAIN_PATH" >> $GITHUB_ENV
# Install provisioning profile
- name: Install Provisioning Profile
env:
PROVISIONING_PROFILE: ${{ secrets.PROVISIONING_PROFILE }}
run: |
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
echo "$PROVISIONING_PROFILE" | base64 --decode \
> ~/Library/MobileDevice/Provisioning\ Profiles/distribution.mobileprovision
# Setup App Store Connect API
- name: Setup App Store Connect API
env:
API_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}
API_KEY_CONTENT: ${{ secrets.APP_STORE_CONNECT_API_KEY_CONTENT }}
run: |
mkdir -p ~/.private_keys
echo "$API_KEY_CONTENT" > ~/.private_keys/AuthKey_$API_KEY_ID.p8
# Increment build number (optional)
- name: Increment Build Number
if: ${{ inputs.bump_build != false }}
run: |
# Get current build number and increment
BUILD_NUMBER=$(date +%Y%m%d%H%M)
echo "BUILD_NUMBER=$BUILD_NUMBER" >> $GITHUB_ENV
# Update Info.plist or use agvtool
# agvtool new-version -all $BUILD_NUMBER
# Build archive
- name: Build Archive
run: |
xcodebuild archive \
-scheme "$SCHEME" \
-archivePath build/$PRODUCT_NAME.xcarchive \
-destination 'generic/platform=iOS' \
-clonedSourcePackagesDirPath SourcePackages \
CODE_SIGN_STYLE=Manual \
| xcbeautify --renderer github-actions
# Create ExportOptions.plist
- name: Create Export Options
run: |
cat > build/ExportOptions.plist << EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>method</key>
<string>app-store</string>
<key>destination</key>
<string>upload</string>
<key>signingStyle</key>
<string>manual</string>
<key>provisioningProfiles</key>
<dict>
<key>YOUR_BUNDLE_ID</key>
<string>YOUR_PROFILE_NAME</string>
</dict>
</dict>
</plist>
EOF
# Export IPA
- name: Export IPA
run: |
xcodebuild -exportArchive \
-archivePath build/$PRODUCT_NAME.xcarchive \
-exportPath build/Export \
-exportOptionsPlist build/ExportOptions.plist \
| xcbeautify --renderer github-actions
# Upload to TestFlight
- name: Upload to TestFlight
env:
API_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}
API_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_API_ISSUER_ID }}
run: |
xcrun altool --upload-app \
--type ios \
--file "build/Export/$PRODUCT_NAME.ipa" \
--apiKey "$API_KEY_ID" \
--apiIssuer "$API_ISSUER_ID"
# Cleanup
- name: Cleanup Keychain
if: always()
run: |
security delete-keychain $KEYCHAIN_PATH || true
# Upload artifacts
- name: Upload Archive
uses: actions/upload-artifact@v4
with:
name: archive
path: build/${{ env.PRODUCT_NAME }}.xcarchive
retention-days: 30
- name: Upload IPA
uses: actions/upload-artifact@v4
with:
name: ipa
path: build/Export/${{ env.PRODUCT_NAME }}.ipa
retention-days: 30
#!/bin/bash
# ci_post_clone.sh
# Xcode Cloud: Runs after the repository is cloned
#
# Use this script for:
# - Installing additional dependencies (Homebrew, etc.)
# - Setting up environment variables
# - Configuring build settings
#
# Available environment variables:
# - CI_WORKSPACE: Path to workspace
# - CI_XCODE_PROJECT: Project path
# - CI_XCODE_SCHEME: Scheme name
# - CI_BRANCH: Git branch name
# - CI_COMMIT: Commit SHA
# - CI_BUILD_NUMBER: Xcode Cloud build number
set -e # Exit on error
echo "📦 Running post-clone script..."
echo "Branch: $CI_BRANCH"
echo "Commit: $CI_COMMIT"
echo "Build: $CI_BUILD_NUMBER"
# ============================================
# Install Homebrew dependencies (if needed)
# ============================================
# Uncomment to install Homebrew packages
# if command -v brew &> /dev/null; then
# echo "Installing Homebrew dependencies..."
# brew install swiftlint
# brew install xcbeautify
# fi
# ============================================
# Install Ruby dependencies (if using fastlane)
# ============================================
# Uncomment if using fastlane
# if [ -f "Gemfile" ]; then
# echo "Installing Ruby dependencies..."
# bundle install
# fi
# ============================================
# Environment-specific configuration
# ============================================
# Configure based on branch
case "$CI_BRANCH" in
"main" | "master")
echo "Production build configuration"
# Set production environment variables
export APP_ENV="production"
;;
"develop")
echo "Staging build configuration"
export APP_ENV="staging"
;;
*)
echo "Development build configuration"
export APP_ENV="development"
;;
esac
# ============================================
# Generate build configuration file
# ============================================
# Example: Create a BuildConfig.swift with build info
# Uncomment and customize as needed
# cat > "$CI_WORKSPACE/BuildConfig.swift" << EOF
# // Auto-generated by CI
# import Foundation
#
# enum BuildConfig {
# static let buildNumber = "$CI_BUILD_NUMBER"
# static let commitHash = "$CI_COMMIT"
# static let branch = "$CI_BRANCH"
# static let environment = "$APP_ENV"
# }
# EOF
# ============================================
# Fetch secrets from environment
# ============================================
# Access custom environment variables set in Xcode Cloud workflow
# These are set in App Store Connect > Xcode Cloud > Workflow > Environment
# if [ -n "$API_BASE_URL" ]; then
# echo "API Base URL configured"
# fi
# if [ -n "$SENTRY_DSN" ]; then
# echo "Sentry DSN configured"
# fi
echo "✅ Post-clone script completed"
#!/bin/bash
# ci_pre_xcodebuild.sh
# Xcode Cloud: Runs before xcodebuild starts
#
# Use this script for:
# - Running code generation (SwiftGen, Sourcery, etc.)
# - Updating version/build numbers
# - Final configuration before build
#
# Available environment variables:
# - CI_PRODUCT: Product name
# - CI_XCODE_SCHEME: Scheme being built
# - CI_BUILD_NUMBER: Xcode Cloud build number
# - CI_TAG: Git tag (if triggered by tag)
set -e # Exit on error
echo "🔧 Running pre-xcodebuild script..."
echo "Product: $CI_PRODUCT"
echo "Scheme: $CI_XCODE_SCHEME"
# ============================================
# Code Generation
# ============================================
# SwiftGen (if installed and configured)
# if command -v swiftgen &> /dev/null; then
# echo "Running SwiftGen..."
# swiftgen
# fi
# Sourcery (if installed and configured)
# if command -v sourcery &> /dev/null; then
# echo "Running Sourcery..."
# sourcery
# fi
# ============================================
# Update Build Number
# ============================================
# Option 1: Use Xcode Cloud build number
# This updates the CFBundleVersion in Info.plist
# Find the project
# PROJECT_PATH=$(find "$CI_WORKSPACE" -name "*.xcodeproj" | head -1)
# if [ -n "$PROJECT_PATH" ]; then
# echo "Updating build number to $CI_BUILD_NUMBER..."
# cd "$(dirname "$PROJECT_PATH")"
# agvtool new-version -all "$CI_BUILD_NUMBER"
# fi
# Option 2: Generate build number from date
# BUILD_NUMBER=$(date +%Y%m%d%H%M)
# agvtool new-version -all "$BUILD_NUMBER"
# ============================================
# Update Version for Tags
# ============================================
# If triggered by a tag, extract version
# if [ -n "$CI_TAG" ]; then
# # Extract version from tag (e.g., v1.2.3 -> 1.2.3)
# VERSION=${CI_TAG#v}
# echo "Setting marketing version to $VERSION..."
# agvtool new-marketing-version "$VERSION"
# fi
# ============================================
# Environment-specific Build Settings
# ============================================
# Create xcconfig for environment-specific settings
# Uncomment and customize as needed
# XCCONFIG_PATH="$CI_WORKSPACE/CI.xcconfig"
#
# case "$CI_BRANCH" in
# "main" | "master")
# cat > "$XCCONFIG_PATH" << EOF
# // Production configuration
# API_BASE_URL = https://api.yourapp.com
# ENABLE_ANALYTICS = YES
# EOF
# ;;
# "develop")
# cat > "$XCCONFIG_PATH" << EOF
# // Staging configuration
# API_BASE_URL = https://staging.yourapp.com
# ENABLE_ANALYTICS = YES
# EOF
# ;;
# *)
# cat > "$XCCONFIG_PATH" << EOF
# // Development configuration
# API_BASE_URL = https://dev.yourapp.com
# ENABLE_ANALYTICS = NO
# EOF
# ;;
# esac
# ============================================
# Run Linters
# ============================================
# SwiftLint (optional - can slow down builds)
# if command -v swiftlint &> /dev/null; then
# echo "Running SwiftLint..."
# swiftlint lint --quiet || true # Don't fail on lint warnings
# fi
# ============================================
# Pre-build Checks
# ============================================
# Verify required files exist
# if [ ! -f "$CI_WORKSPACE/GoogleService-Info.plist" ]; then
# echo "⚠️ Warning: GoogleService-Info.plist not found"
# fi
echo "✅ Pre-xcodebuild script completed"
Xcode Cloud Setup Guide
Prerequisites
1. Apple Developer Program membership 2. App Store Connect access with Admin or App Manager role 3. Repository connected to Xcode Cloud (GitHub, GitLab, or Bitbucket)
Initial Setup
1. Enable Xcode Cloud in Xcode
1. Open your project in Xcode 2. Navigate to Product > Xcode Cloud > Create Workflow 3. Sign in to App Store Connect if prompted 4. Select your app or create a new App Store Connect record
2. Connect Repository
1. Choose your source code provider (GitHub, GitLab, Bitbucket) 2. Authorize Xcode Cloud to access your repository 3. Select the repository containing your project
3. Create Your First Workflow
Xcode Cloud will suggest a default workflow. Customize as needed:
Start Conditions
- Branch Changes: Build on push to specific branches
- Pull Request: Build on PR creation/update
- Tag Changes: Build when tags are pushed
- On Schedule: Scheduled builds (e.g., nightly)
Environment
- Select Xcode version
- Select macOS version
- Set custom environment variables
Actions
- Build: Compile the project
- Test: Run unit tests
- Analyze: Static analysis
- Archive: Create deployable archive
Post-Actions
- TestFlight Internal: Distribute to internal testers
- TestFlight External: Distribute to external testers
- App Store: Submit to App Store
Workflow Configurations
Development Workflow (Feature Branches)
Start Condition: Branch starts with "feature/"
Actions: Build, Test
Post-Actions: NonePull Request Workflow
Start Condition: Pull Request to "main"
Actions: Build, Test, Analyze
Post-Actions: NoneBeta Workflow (develop branch)
Start Condition: Push to "develop"
Actions: Build, Test, Archive
Post-Actions: TestFlight InternalRelease Workflow (version tags)
Start Condition: Tag matches "v*.*.*"
Actions: Build, Test, Archive
Post-Actions: TestFlight External, then App StoreEnvironment Variables
Set in App Store Connect > Xcode Cloud > Workflow > Environment:
| Variable | Description | Secret |
|---|---|---|
API_BASE_URL | Backend API URL | No |
SENTRY_DSN | Error tracking DSN | Yes |
ANALYTICS_KEY | Analytics API key | Yes |
FEATURE_FLAGS | Build-time flags | No |
Accessing in Code
// In your Swift code
let apiURL = ProcessInfo.processInfo.environment["API_BASE_URL"]Accessing in ci_scripts
# In ci_scripts
echo "API URL: $API_BASE_URL"ci_scripts
Place scripts in ci_scripts/ directory at repository root:
| Script | When it runs |
|---|---|
ci_post_clone.sh | After repository clone |
ci_pre_xcodebuild.sh | Before xcodebuild |
ci_post_xcodebuild.sh | After successful build |
Making Scripts Executable
chmod +x ci_scripts/*.sh
git add ci_scripts/
git commit -m "Add Xcode Cloud ci_scripts"Code Signing
Xcode Cloud manages code signing automatically:
1. Automatic: Xcode Cloud generates certificates and profiles 2. Manual: Use your own certificates (uploaded to App Store Connect)
For Automatic Signing
- Ensure "Automatically manage signing" is enabled in Xcode
- Xcode Cloud will create necessary certificates/profiles
For Manual Signing
1. Upload certificates to App Store Connect 2. Create provisioning profiles in App Store Connect 3. Reference profiles in your Xcode project
Build Number Management
Xcode Cloud provides CI_BUILD_NUMBER environment variable.
Option 1: Use in ci_pre_xcodebuild.sh
agvtool new-version -all "$CI_BUILD_NUMBER"Option 2: Configure in Build Settings
Set CURRENT_PROJECT_VERSION to $(CI_BUILD_NUMBER) in Xcode.
Troubleshooting
Build Fails: Missing Dependencies
Add dependency installation to ci_post_clone.sh:
brew install swiftlintBuild Fails: Code Signing
1. Verify App Store Connect has correct bundle ID 2. Check provisioning profiles are valid 3. Ensure certificates haven't expired
Tests Fail: Simulator Issues
Specify exact simulator in test action:
- Destination: iPhone 16
- OS Version: Latest
Slow Builds
1. Use selective trigger conditions 2. Avoid installing unnecessary dependencies 3. Use caching where possible
Monitoring
In Xcode
- Report Navigator > Cloud tab shows all builds
- Click any build for detailed logs
In App Store Connect
- Xcode Cloud section shows all workflows and builds
- Download build artifacts
- View test results
Notifications
Configure in App Store Connect:
- Email notifications for build status
- Slack integration (via webhooks in ci_scripts)
Best Practices
1. Use ci_scripts sparingly - Keep builds fast 2. Cache dependencies - Homebrew packages persist 3. Use secrets for sensitive data - Never hardcode API keys 4. Test locally first - Use xcodebuild commands locally 5. Monitor build minutes - Xcode Cloud has usage limits
Pricing
Xcode Cloud includes:
- 25 compute hours/month free (Apple Developer Program)
- Additional hours available for purchase
- Build minutes vary by Xcode version and machine type