# iOS CI/CD Pipeline: Enterprise-Grade DevOps 🚀
Merhaba! Bugün iOS projelerinde profesyonel CI/CD pipeline kurulumunu A'dan Z'ye inceleyeceğiz. Bu rehber sonunda, kod push'ladığında otomatik build, test ve TestFlight deployment yapan bir sistem kurmuş olacaksın.
İçindekiler
- CI/CD Nedir ve Neden Önemli?
- GitHub Actions Kurulumu
- Fastlane Kurulumu
- Secrets Management
- Test Automation
- Code Quality ve Metrics
- Sürpriz Hediye: Complete CI/CD Checklist
- Xcode Cloud Alternatifi
- Pro Tips ve Best Practices
- Easter Egg: Debug Workflow
- Sonuç
🎯 Bu Yazıda Öğreneceklerin
- GitHub Actions ile iOS CI/CD kurulumu
- Fastlane ile build automation
- Match ile code signing yönetimi
- Xcode Cloud alternatifi
- Test automation ve code coverage
- Security best practices
- Monitoring ve alerting
📚 CI/CD Nedir ve Neden Önemli?
CI (Continuous Integration): Her commit'te otomatik build ve test
CD (Continuous Delivery/Deployment): Otomatik TestFlight/App Store dağıtımı
iOS CI/CD Araçları Karşılaştırması
Araç | Maliyet | macOS Runner | Kurulum |
|---|---|---|---|
GitHub Actions | 2000 dk/ay ücretsiz | ✅ | Orta |
Xcode Cloud | 25 saat/ay ücretsiz | ✅ | Kolay |
Bitrise | Sınırlı ücretsiz | ✅ | Kolay |
CircleCI | Sınırlı ücretsiz | ✅ | Orta |
Jenkins | Ücretsiz | Self-hosted | Zor |
Fastlane | Ücretsiz (local) | Local | Kolay |
💡 Altın İpucu: Başlangıç için Xcode Cloud, scale için GitHub Actions + Fastlane combo'su ideal!
Dış Kaynaklar:
🏗️ GitHub Actions Kurulumu
1. Temel Workflow Yapısı
yaml
1# .github/workflows/ios-ci.yml2name: iOS CI/CD Pipeline3 4on:5 push:6 branches: [main, develop]7 pull_request:8 branches: [main]9 # Manual trigger10 workflow_dispatch:11 inputs:12 deploy_target:13 description: 'Deploy target'14 required: true15 default: 'testflight'16 type: choice17 options:18 - testflight19 - app-store20 21# Concurrent run'ları iptal et22concurrency:23 group: ${{ github.workflow }}-${{ github.ref }}24 cancel-in-progress: true25 26env:27 XCODE_VERSION: '15.2'28 SCHEME: 'MyApp'29 PROJECT: 'MyApp.xcodeproj'30 # Veya xcworkspace için:31 # WORKSPACE: 'MyApp.xcworkspace'32 33jobs:34 # Job 1: Lint & Format Check35 lint:36 runs-on: macos-1437 steps:38 - uses: actions/checkout@v439 40 - name: Install SwiftLint41 run: brew install swiftlint42 43 - name: Run SwiftLint44 run: swiftlint --strict --reporter github-actions-logging45 46 - name: Check Swift Format47 run: |48 brew install swift-format49 swift-format lint --recursive Sources/ Tests/50 51 # Job 2: Build & Test52 build-and-test:53 needs: lint54 runs-on: macos-1455 timeout-minutes: 3056 57 steps:58 - name: Checkout59 uses: actions/checkout@v460 with:61 fetch-depth: 0 # Full history for versioning62 63 - name: Setup Xcode64 uses: maxim-lobanov/setup-xcode@v165 with:66 xcode-version: ${{ env.XCODE_VERSION }}67 68 # SPM Cache69 - name: Cache SPM70 uses: actions/cache@v471 with:72 path: |73 ~/Library/Developer/Xcode/DerivedData/**/SourcePackages74 .build75 key: ${{ runner.os }}-spm-${{ hashFiles('**/Package.resolved') }}76 restore-keys: |77 ${{ runner.os }}-spm-78 79 # Ruby/Bundler for Fastlane80 - name: Setup Ruby81 uses: ruby/setup-ruby@v182 with:83 ruby-version: '3.2'84 bundler-cache: true85 86 # Build87 - name: Build for Testing88 run: |89 set -o pipefail90 xcodebuild build-for-testing \91 -scheme "${{ env.SCHEME }}" \92 -destination 'platform=iOS Simulator,name=iPhone 15 Pro' \93 -configuration Debug \94 -derivedDataPath DerivedData \95 CODE_SIGNING_ALLOWED=NO \96 | xcpretty --color97 98 # Unit Tests99 - name: Run Unit Tests100 run: |101 set -o pipefail102 xcodebuild test-without-building \103 -scheme "${{ env.SCHEME }}" \104 -destination 'platform=iOS Simulator,name=iPhone 15 Pro' \105 -derivedDataPath DerivedData \106 -resultBundlePath TestResults.xcresult \107 -enableCodeCoverage YES \108 | xcpretty --color --report junit --output test-results.xml109 110 # UI Tests (opsiyonel)111 - name: Run UI Tests112 if: github.event_name == 'push' && github.ref == 'refs/heads/main'113 run: |114 xcodebuild test \115 -scheme "${{ env.SCHEME }}UITests" \116 -destination 'platform=iOS Simulator,name=iPhone 15 Pro' \117 -derivedDataPath DerivedData \118 | xcpretty119 120 # Upload test results121 - name: Upload Test Results122 uses: actions/upload-artifact@v4123 if: always()124 with:125 name: test-results126 path: |127 TestResults.xcresult128 test-results.xml129 retention-days: 14130 131 # Code Coverage132 - name: Generate Coverage Report133 run: |134 xcrun xccov view --report TestResults.xcresult > coverage.txt135 cat coverage.txt136 137 - name: Upload Coverage to Codecov138 uses: codecov/codecov-action@v4139 with:140 xcode: true141 xcode_archive_path: TestResults.xcresult142 143 # Job 3: Deploy to TestFlight144 deploy-testflight:145 needs: build-and-test146 runs-on: macos-14147 if: github.ref == 'refs/heads/main' && github.event_name == 'push'148 environment: production149 150 steps:151 - uses: actions/checkout@v4152 153 - uses: maxim-lobanov/setup-xcode@v1154 with:155 xcode-version: ${{ env.XCODE_VERSION }}156 157 - uses: ruby/setup-ruby@v1158 with:159 ruby-version: '3.2'160 bundler-cache: true161 162 # App Store Connect API Key163 - name: Setup ASC API Key164 env:165 ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }}166 ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}167 ASC_KEY_CONTENT: ${{ secrets.ASC_KEY_CONTENT }}168 run: |169 mkdir -p ~/.appstoreconnect/private_keys170 echo "$ASC_KEY_CONTENT" > ~/.appstoreconnect/private_keys/AuthKey_$ASC_KEY_ID.p8171 172 # Deploy via Fastlane173 - name: Deploy to TestFlight174 env:175 MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}176 MATCH_GIT_PRIVATE_KEY: ${{ secrets.MATCH_GIT_PRIVATE_KEY }}177 ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }}178 ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}179 FASTLANE_XCODEBUILD_SETTINGS_TIMEOUT: 120180 run: |181 bundle exec fastlane ios beta182 183 # Slack/Discord notification184 - name: Notify Success185 if: success()186 uses: slackapi/slack-github-action@v1187 with:188 payload: |189 {190 "text": "✅ New build deployed to TestFlight!",191 "blocks": [192 {193 "type": "section",194 "text": {195 "type": "mrkdwn",196 "text": "*MyApp* - Build #${{ github.run_number }} deployed to TestFlight"197 }198 }199 ]200 }201 env:202 SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}🔧 Fastlane Kurulumu
2. Fastfile Konfigürasyonu
ruby
1# fastlane/Fastfile2default_platform(:ios)3 4# ═══════════════════════════════════════════5# CONSTANTS6# ═══════════════════════════════════════════7APP_IDENTIFIER = "com.mycompany.myapp"8TEAM_ID = "XXXXXXXXXX"9ITC_TEAM_ID = "XXXXXXXXXX"10 11# ═══════════════════════════════════════════12# iOS Platform13# ═══════════════════════════════════════════14platform :ios do15 16 # ─────────────────────────────────────────17 # Before All18 # ─────────────────────────────────────────19 before_all do20 # CI ortamında keychain setup21 setup_ci if ENV['CI']22 23 # Version info24 UI.message("🚀 Running Fastlane on: #{lane_context[:PLATFORM_NAME]}")25 end26 27 # ─────────────────────────────────────────28 # Error Handler29 # ─────────────────────────────────────────30 error do |lane, exception|31 slack(32 message: "❌ Fastlane failed: #{exception.message}",33 success: false,34 slack_url: ENV['SLACK_WEBHOOK']35 ) if ENV['SLACK_WEBHOOK']36 end37 38 # ═══════════════════════════════════════════39 # LANES40 # ═══════════════════════════════════════════41 42 # ─────────────────────────────────────────43 # Test Lane44 # ─────────────────────────────────────────45 desc "Run all tests"46 lane :test do47 scan(48 scheme: "MyApp",49 device: "iPhone 15 Pro",50 clean: true,51 code_coverage: true,52 output_types: "junit,html",53 output_directory: "./fastlane/test_output",54 result_bundle: true,55 xcargs: "-skipPackagePluginValidation"56 )57 end58 59 # ─────────────────────────────────────────60 # Beta Lane (TestFlight)61 # ─────────────────────────────────────────62 desc "Build and deploy to TestFlight"63 lane :beta do |options|64 # Ensure clean state65 ensure_git_status_clean unless options[:skip_git_check]66 67 # Increment build number68 increment_build_number(69 build_number: ENV['GITHUB_RUN_NUMBER'] || latest_testflight_build_number + 170 )71 72 # Code signing73 match(74 type: "appstore",75 readonly: is_ci,76 app_identifier: APP_IDENTIFIER77 )78 79 # Build80 build_ios_app(81 scheme: "MyApp",82 configuration: "Release",83 export_method: "app-store",84 export_options: {85 provisioningProfiles: {86 APP_IDENTIFIER => "match AppStore #{APP_IDENTIFIER}"87 }88 },89 xcargs: "-skipPackagePluginValidation",90 include_bitcode: false,91 output_directory: "./build",92 output_name: "MyApp.ipa"93 )94 95 # Upload to TestFlight96 upload_to_testflight(97 api_key: app_store_connect_api_key,98 skip_waiting_for_build_processing: true,99 changelog: changelog_from_git_commits(100 commits_count: 10,101 merge_commit_filtering: "only_include_merges"102 )103 )104 105 # Tag release106 add_git_tag(107 tag: "build/#{lane_context[SharedValues::BUILD_NUMBER]}"108 )109 110 # Notify111 slack(112 message: "🚀 New build uploaded to TestFlight!",113 success: true,114 slack_url: ENV['SLACK_WEBHOOK'],115 default_payloads: [:git_branch, :git_author]116 ) if ENV['SLACK_WEBHOOK']117 end118 119 # ─────────────────────────────────────────120 # Release Lane (App Store)121 # ─────────────────────────────────────────122 desc "Deploy to App Store"123 lane :release do |options|124 # Version bump125 version = options[:version] || prompt(text: "Enter version number:")126 increment_version_number(version_number: version)127 increment_build_number(build_number: 1)128 129 # Build130 match(type: "appstore", readonly: true)131 132 build_ios_app(133 scheme: "MyApp",134 configuration: "Release",135 export_method: "app-store"136 )137 138 # Upload with metadata139 upload_to_app_store(140 api_key: app_store_connect_api_key,141 skip_screenshots: true,142 skip_metadata: false,143 precheck_include_in_app_purchases: false,144 submit_for_review: false,145 automatic_release: false,146 force: true147 )148 149 # Commit and push150 commit_version_bump(151 message: "Release v#{version}",152 xcodeproj: "MyApp.xcodeproj"153 )154 155 add_git_tag(tag: "v#{version}")156 push_to_git_remote157 end158 159 # ─────────────────────────────────────────160 # Certificates Lane161 # ─────────────────────────────────────────162 desc "Sync certificates"163 lane :sync_certs do164 match(type: "development", readonly: false)165 match(type: "appstore", readonly: false)166 end167 168 # ─────────────────────────────────────────169 # Screenshots Lane170 # ─────────────────────────────────────────171 desc "Capture screenshots"172 lane :screenshots do173 capture_screenshots(174 workspace: "MyApp.xcworkspace",175 scheme: "MyAppUITests",176 devices: [177 "iPhone 15 Pro Max",178 "iPhone SE (3rd generation)",179 "iPad Pro (12.9-inch) (6th generation)"180 ],181 languages: ["en-US", "tr"],182 output_directory: "./fastlane/screenshots"183 )184 185 frame_screenshots(186 path: "./fastlane/screenshots",187 silver: true188 )189 end190 191 # ═══════════════════════════════════════════192 # PRIVATE LANES193 # ═══════════════════════════════════════════194 195 private_lane :app_store_connect_api_key do196 app_store_connect_api_key(197 key_id: ENV["ASC_KEY_ID"],198 issuer_id: ENV["ASC_ISSUER_ID"],199 key_filepath: "~/.appstoreconnect/private_keys/AuthKey_#{ENV['ASC_KEY_ID']}.p8",200 in_house: false201 )202 end203end3. Matchfile (Code Signing)
ruby
1# fastlane/Matchfile2git_url("[email protected]:mycompany/ios-certificates.git")3storage_mode("git")4 5type("appstore")6app_identifier(["com.mycompany.myapp"])7username("[email protected]")8 9# Team settings10team_id("XXXXXXXXXX")11team_name("My Company")12 13# For CI14readonly(is_ci)15 16# 🐣 Easter Egg: Gizli bypass17# ENV['MATCH_SKIP_CONFIRM']='1' ile confirmation atla🔐 Secrets Management
4. GitHub Secrets Kurulumu
yaml
1# Required secrets (Settings > Secrets > Actions):2 3# App Store Connect API4ASC_KEY_ID: "XXXXXXXXXX"5ASC_ISSUER_ID: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"6ASC_KEY_CONTENT: |7 -----BEGIN PRIVATE KEY-----8 MIGTAgEAMB....9 -----END PRIVATE KEY-----10 11# Match (Code Signing)12MATCH_PASSWORD: "your-encryption-password"13MATCH_GIT_PRIVATE_KEY: |14 -----BEGIN OPENSSH PRIVATE KEY-----15 xxxxx...16 -----END OPENSSH PRIVATE KEY-----17 18# Notifications19SLACK_WEBHOOK: "https://hooks.slack.com/services/..."5. Keychain Yönetimi
ruby
1# fastlane/actions/setup_ci_keychain.rb2# Custom action for CI keychain3 4module Fastlane5 module Actions6 class SetupCiKeychainAction < Action7 def self.run(params)8 keychain_name = "fastlane_ci_keychain"9 keychain_password = SecureRandom.base64(32)10 11 # Create keychain12 create_keychain(13 name: keychain_name,14 password: keychain_password,15 default_keychain: true,16 unlock: true,17 timeout: 3600,18 lock_when_sleeps: false19 )20 21 # Return for cleanup22 { name: keychain_name, password: keychain_password }23 end24 end25 end26end🧪 Test Automation
6. Test Matrix Workflow
yaml
1# .github/workflows/test-matrix.yml2name: Test Matrix3 4on:5 pull_request:6 branches: [main, develop]7 8jobs:9 test-matrix:10 strategy:11 fail-fast: false12 matrix:13 os: [macos-13, macos-14]14 xcode: ['15.0', '15.2']15 destination:16 - 'platform=iOS Simulator,name=iPhone 15,OS=17.2'17 - 'platform=iOS Simulator,name=iPhone 14,OS=16.4'18 19 runs-on: ${{ matrix.os }}20 name: Xcode ${{ matrix.xcode }} - ${{ matrix.destination }}21 22 steps:23 - uses: actions/checkout@v424 25 - uses: maxim-lobanov/setup-xcode@v126 with:27 xcode-version: ${{ matrix.xcode }}28 29 - name: Run Tests30 run: |31 xcodebuild test \32 -scheme "MyApp" \33 -destination '${{ matrix.destination }}' \34 -resultBundlePath "Results-${{ matrix.xcode }}.xcresult" \35 CODE_SIGNING_ALLOWED=NO36 37 - name: Upload Results38 uses: actions/upload-artifact@v439 with:40 name: test-results-${{ matrix.xcode }}41 path: "*.xcresult"📊 Code Quality & Metrics
7. Quality Gates
yaml
1# .github/workflows/quality.yml2name: Code Quality3 4on: [pull_request]5 6jobs:7 quality-gate:8 runs-on: macos-149 10 steps:11 - uses: actions/checkout@v412 with:13 fetch-depth: 014 15 # SwiftLint16 - name: SwiftLint17 run: |18 brew install swiftlint19 swiftlint --strict --reporter json > swiftlint-report.json20 21 # Complexity Analysis22 - name: Analyze Complexity23 run: |24 brew install periphery25 periphery scan --format json > periphery-report.json26 27 # Dead Code Detection28 - name: Dead Code Check29 run: |30 periphery scan --format xcode31 32 # Dependency Check33 - name: Check Outdated Dependencies34 run: |35 swift package show-dependencies --format json36 37 # Size Analysis38 - name: Binary Size Check39 run: |40 xcodebuild -scheme MyApp -configuration Release archive \41 -archivePath MyApp.xcarchive \42 CODE_SIGNING_ALLOWED=NO43 44 # Check size45 du -sh MyApp.xcarchive/Products/Applications/MyApp.app46 47 # Report48 - name: Generate Report49 run: |50 echo "## Code Quality Report" >> $GITHUB_STEP_SUMMARY51 echo "- SwiftLint issues: $(cat swiftlint-report.json | jq length)" >> $GITHUB_STEP_SUMMARYmarkdown
1# 🎁 iOS CI/CD PRODUCTION CHECKLIST2 3## GitHub Actions Setup4- [ ] Workflow files oluşturuldu (.github/workflows/)5- [ ] macOS runner version belirlendi6- [ ] Xcode version sabitlendi7- [ ] SPM cache aktif8- [ ] Concurrent run cancellation aktif9 10## Fastlane Setup11- [ ] Fastfile oluşturuldu12- [ ] Matchfile konfigüre edildi13- [ ] Appfile oluşturuldu14- [ ] Gemfile dependencies tanımlandı15- [ ] .env.default dosyası oluşturuldu16 17## Code Signing18- [ ] Match repository oluşturuldu19- [ ] Development certificate sync20- [ ] App Store certificate sync21- [ ] Provisioning profiles sync22- [ ] CI keychain automation23 24## Secrets25- [ ] ASC_KEY_ID26- [ ] ASC_ISSUER_ID27- [ ] ASC_KEY_CONTENT28- [ ] MATCH_PASSWORD29- [ ] MATCH_GIT_PRIVATE_KEY30- [ ] SLACK_WEBHOOK (optional)31 32## Testing33- [ ] Unit test lane34- [ ] UI test lane35- [ ] Code coverage reporting36- [ ] Test matrix (multiple iOS versions)37- [ ] Snapshot testing38 39## Deployment40- [ ] TestFlight deployment41- [ ] App Store deployment42- [ ] Changelog generation43- [ ] Git tagging44- [ ] Release notes45 46## Monitoring47- [ ] Slack notifications48- [ ] Build time tracking49- [ ] Test coverage trends50- [ ] Error alerting51 52## Security53- [ ] Secrets rotation schedule54- [ ] Access audit55- [ ] Dependency vulnerability scan56- [ ] Code signing auditOkuyucu Ödülü
⚡ Xcode Cloud Alternatifi
8. Xcode Cloud Kurulumu
swift
1// ci_scripts/ci_post_clone.sh2#!/bin/sh3 4set -e5 6echo "🔧 Post-clone setup..."7 8# Install dependencies9if [ -f "Brewfile" ]; then10 brew bundle install11fi12 13# Install Ruby gems14if [ -f "Gemfile" ]; then15 bundle install16fi17 18# SwiftLint19if command -v swiftlint &> /dev/null; then20 swiftlint --strict21fi22 23echo "✅ Post-clone complete"24 25// ci_scripts/ci_pre_xcodebuild.sh26#!/bin/sh27 28set -e29 30echo "🏗️ Pre-build setup..."31 32# Increment build number33if [ "$CI_XCODE_CLOUD" = "TRUE" ]; then34 agvtool new-version -all $CI_BUILD_NUMBER35fi36 37echo "✅ Pre-build complete"38 39// ci_scripts/ci_post_xcodebuild.sh40#!/bin/sh41 42set -e43 44if [ "$CI_XCODEBUILD_EXIT_CODE" != "0" ]; then45 echo "❌ Build failed with code: $CI_XCODEBUILD_EXIT_CODE"46 exit 147fi48 49echo "✅ Build succeeded"50 51# Upload dSYMs to Crashlytics52if [ "$CI_XCODEBUILD_ACTION" = "archive" ]; then53 echo "📤 Uploading dSYMs..."54 # Firebase Crashlytics upload script55fi💡 Pro Tips ve Best Practices
- Cache Everything - SPM, CocoaPods, DerivedData cache'le.
- Parallel Jobs - Lint, build, test'i paralel çalıştır.
- Fail Fast - İlk hata'da dur, zaman kaybet.
- macOS 14 Kullan - M1 runner'lar 3x daha hızlı.
- Self-hosted Runner - Yoğun projeler için kendi Mac mini'ni kur.
- Artifact Retention - Test sonuçlarını 14 gün tut, daha fazlası gereksiz.
- Environment Separation - Staging/production için ayrı workflow.
- Secrets Rotation - API key'leri 90 günde bir yenile.
yaml
1# Workflow debug için gizli SSH erişimi2# workflow_dispatch input'una ekle:3debug_enabled:4 type: boolean5 description: 'Enable SSH debug'6 required: false7 default: false8 9# Job'a ekle:10- name: Debug SSH11 if: ${{ inputs.debug_enabled }}12 uses: mxschmitt/action-tmate@v313 with:14 limit-access-to-actor: true📖 Sonuç
CI/CD, modern iOS development'ın olmazsa olmazı. GitHub Actions + Fastlane combo'su ile profesyonel düzeyde pipeline kurabilirsin.
Unutma: Başlangıçta 1-2 gün yatırım, sonrasında yüzlerce saat tasarruf!
Bir sonraki yazıda görüşmek üzere! 🚀
*CI/CD kurulumunda takıldın mı? Twitter'dan yaz!*
Easter Egg
Gizli bir bilgi buldun!
Bu bölümde gizli bir bilgi var. Keşfetmek ister misin?
ALTIN İPUCU
Bu yazının en değerli bilgisi
Bu ipucu, yazının en önemli çıkarımını içeriyor.

