Flutter CI/CD: Otomatik Build, Test ve Release Süreci

13 dakika okuma9 Şubat 2026Güncellendi: 9 Mar 2026
Flutter CI/CDFlutter GitHub ActionsFlutter CodemagicFlutter FastlaneFlutter release automationFlutter build pipelineFlutter deploymentmobile CI/CD

# Flutter CI/CD: Otomatik Build, Test ve Release Pipeline

Bir Flutter uygulamasını elle yayınlamak yavaş, hataya açık ve stresli bir süreçtir. İyi tasarlanmış bir CI/CD pipeline, release sürecindeki insan kaynaklı darboğazları ortadan kaldırır ve build'leri takip etmek yerine kod yazmaya odaklanmanızı sağlar. Bu yazıda, linting aşamasından mağaza dağıtımına kadar üretimde kullandığım pipeline'ın tamamını gerçek yapılandırma örnekleriyle anlatıyorum.

Flutter İçin CI/CD Neden Önemli

Flutter tek kod tabanından birden fazla platformu hedefler. Bu da her release'in potansiyel olarak bir Android APK/AAB, bir iOS IPA, bir web build ve masaüstü artifact'leri içerdiği anlamına gelir. Bunu elle yapmak sadece sıkıcı değil, güvenilirlik açısından da risk taşır. CI/CD pipeline size şunları kazandırır:

  • **Tutarlılık**: Her build aynı adımları aynı sırayla çalıştırır.
  • **Hız**: Paralel çalışan job'lar geri bildirim döngünüzü saatlerden dakikalara indirir.
  • **Güven**: Otomatik testler, regresyonları kullanıcılara ulaşmadan yakalar.
  • **İzlenebilirlik**: Her artifact belirli bir commit'e bağlıdır.
  • Temel Pipeline Aşamaları

    Sağlam bir Flutter CI/CD pipeline dört aşamadan oluşur:

  • **Kod kalite kontrolleri** — lint, format, statik analiz
  • **Otomatik testler** — unit, widget ve entegrasyon testleri
  • **Build ve artifact oluşturma** — platforma özel binary'ler
  • **Release ve dağıtım** — mağaza yüklemeleri, beta kanalları, changelog'lar
  • Her aşama bir kapı görevi görür. Herhangi bir adım başarısız olursa pipeline durur ve ekip anında bilgilendirilir.

    GitHub Actions Workflow

    GitHub Actions, Flutter CI/CD için en çok tercih ettiğim araç. Analiz, test ve her iki platform için build aşamalarını kapsayan, üretime hazır bir workflow:

    yaml
    name: Flutter CI/CD
    
    on:
      push:
        branches: [main, develop]
      pull_request:
        branches: [main]
    
    env:
      FLUTTER_VERSION: "3.24.0"
      JAVA_VERSION: "17"
    
    jobs:
      analyze-and-test:
        name: Analiz & Test
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v4
    
          - uses: subosito/flutter-action@v2
            with:
              flutter-version: ${{ env.FLUTTER_VERSION }}
              cache: true
    
          - name: Bağımlılıkları yükle
            run: flutter pub get
    
          - name: Format kontrolü
            run: dart format --set-exit-if-changed .
    
          - name: Analiz çalıştır
            run: flutter analyze --fatal-infos
    
          - name: Testleri coverage ile çalıştır
            run: flutter test --coverage
    
          - name: Coverage yükle
            uses: codecov/codecov-action@v4
            with:
              file: coverage/lcov.info
    
      build-android:
        name: Android Build
        needs: analyze-and-test
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v4
    
          - uses: actions/setup-java@v4
            with:
              distribution: "temurin"
              java-version: ${{ env.JAVA_VERSION }}
    
          - uses: subosito/flutter-action@v2
            with:
              flutter-version: ${{ env.FLUTTER_VERSION }}
              cache: true
    
          - name: Keystore decode
            env:
              KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
            run: echo "$KEYSTORE_BASE64" | base64 --decode > android/app/release.keystore
    
          - name: key.properties oluştur
            env:
              KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
              KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
              STORE_PASSWORD: ${{ secrets.ANDROID_STORE_PASSWORD }}
            run: |
              cat > android/key.properties <<EOF
              storePassword=$STORE_PASSWORD
              keyPassword=$KEY_PASSWORD
              keyAlias=$KEY_ALIAS
              storeFile=release.keystore
              EOF
    
          - name: App Bundle oluştur
            run: flutter build appbundle --release
    
          - name: Artifact yükle
            uses: actions/upload-artifact@v4
            with:
              name: android-release
              path: build/app/outputs/bundle/release/*.aab
    
      build-ios:
        name: iOS Build
        needs: analyze-and-test
        runs-on: macos-latest
        steps:
          - uses: actions/checkout@v4
    
          - uses: subosito/flutter-action@v2
            with:
              flutter-version: ${{ env.FLUTTER_VERSION }}
              cache: true
    
          - name: CocoaPods yükle
            run: cd ios && pod install
    
          - name: İmzalama sertifikasını içe aktar
            env:
              P12_BASE64: ${{ secrets.IOS_P12_BASE64 }}
              P12_PASSWORD: ${{ secrets.IOS_P12_PASSWORD }}
            run: |
              echo "$P12_BASE64" | base64 --decode > certificate.p12
              security create-keychain -p "" build.keychain
              security import certificate.p12 -k build.keychain -P "$P12_PASSWORD" -T /usr/bin/codesign
              security set-key-partition-list -S apple-tool:,apple: -s -k "" build.keychain
              security list-keychains -d user -s build.keychain
    
          - name: Provisioning profile yükle
            env:
              PROFILE_BASE64: ${{ secrets.IOS_PROVISIONING_PROFILE_BASE64 }}
            run: |
              echo "$PROFILE_BASE64" | base64 --decode > profile.mobileprovision
              mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
              cp profile.mobileprovision ~/Library/MobileDevice/Provisioning\ Profiles/
    
          - name: IPA oluştur
            run: flutter build ipa --release --export-options-plist=ios/ExportOptions.plist
    
          - name: Artifact yükle
            uses: actions/upload-artifact@v4
            with:
              name: ios-release
              path: build/ios/ipa/*.ipa

    Kendi release pipeline'larımda Flutter sürümünü her zaman sabit tutarım. "Latest" sürüme güvenmek, pazartesi sabahı sürpriz hatalarla karşılaşmanın en kısa yoludur.

    Codemagic Yapılandırması

    Codemagic, Flutter'a özel olarak geliştirilmiş bir CI/CD servisidir. Kod imzalama ve mağaza dağıtımını minimum yapılandırmayla gerçekleştirir. İşte bir `codemagic.yaml` yapılandırması:

    yaml
    workflows:
      production-release:
        name: Production Release
        max_build_duration: 60
        instance_type: mac_mini_m2
    
        environment:
          flutter: 3.24.0
          xcode: latest
          groups:
            - google_play_credentials
            - app_store_credentials
            - code_signing_ios
          vars:
            APP_ID: com.example.myflutterapp
    
        triggering:
          events:
            - tag
          tag_patterns:
            - pattern: "v*"
              include: true
    
        scripts:
          - name: Bağımlılıkları yükle
            script: flutter pub get
    
          - name: Analiz çalıştır
            script: flutter analyze --fatal-infos
    
          - name: Testleri çalıştır
            script: flutter test
    
          - name: iOS kod imzalama ayarla
            script: |
              keychain initialize
              app-store-connect fetch-signing-files "$APP_ID" \
                --type IOS_APP_STORE \
                --create
              keychain add-certificates
    
          - name: iOS build
            script: |
              flutter build ipa --release \
                --build-number=$PROJECT_BUILD_NUMBER \
                --export-options-plist=/Users/builder/export_options.plist
    
          - name: Android build
            script: |
              flutter build appbundle --release \
                --build-number=$PROJECT_BUILD_NUMBER
    
        artifacts:
          - build/ios/ipa/*.ipa
          - build/app/outputs/bundle/release/*.aab
    
        publishing:
          app_store_connect:
            auth: integration
            submit_to_testflight: true
          google_play:
            credentials: $GCLOUD_SERVICE_ACCOUNT_CREDENTIALS
            track: internal

    Tag tabanlı tetikleme yaklaşımını şiddetle tavsiye ediyorum. `v1.2.0` gibi bir tag push'lamak, release sürecinizdeki tek manuel adım olmalıdır.

    Fastlane Entegrasyonu

    Fastlane, CI/CD'nin "son kilometre" kısmını üstlenir — kod imzalama, ekran görüntüleri, metadata ve mağaza yüklemeleri. Build için GitHub Actions veya Codemagic kullansanız bile, mağaza gönderimlerini yönetmek için Fastlane vazgeçilmezdir.

    Android Fastfile

    ruby
    default_platform(:android)
    
    platform :android do
      desc "Google Play dahili kanalına dağıt"
      lane :internal do
        upload_to_play_store(
          track: "internal",
          aab: "../build/app/outputs/bundle/release/app-release.aab",
          json_key_data: ENV["GOOGLE_PLAY_JSON_KEY"],
          skip_upload_metadata: true,
          skip_upload_images: true,
          skip_upload_screenshots: true
        )
      end
    
      desc "Dahili kanaldan üretime yükselt"
      lane :promote_to_production do
        upload_to_play_store(
          track: "internal",
          track_promote_to: "production",
          json_key_data: ENV["GOOGLE_PLAY_JSON_KEY"],
          skip_upload_changelogs: false
        )
      end
    end

    iOS Fastfile

    ruby
    default_platform(:ios)
    
    platform :ios do
      desc "TestFlight'a yeni sürüm gönder"
      lane :beta do
        setup_ci if ENV["CI"]
    
        match(
          type: "appstore",
          app_identifier: "com.example.myflutterapp",
          readonly: is_ci
        )
    
        build_app(
          workspace: "Runner.xcworkspace",
          scheme: "Runner",
          export_method: "app-store"
        )
    
        upload_to_testflight(
          skip_waiting_for_build_processing: true
        )
      end
    
      desc "App Store'a gönder"
      lane :release do
        deliver(
          submit_for_review: true,
          automatic_release: false,
          force: true,
          skip_metadata: false,
          skip_screenshots: true
        )
      end
    end

    Kendi release pipeline'larımda iOS kod imzalama için her zaman `match` kullanırım; çünkü sertifikaları özel bir Git deposunda tutarak tüm ekibin tutarlı bir şekilde erişmesini sağlar.

    iOS ve Android İçin Kod İmzalama

    Kod imzalama, CI/CD kurulumlarının en çok sorun çıkardığı noktadır. Baştan doğru yaparsanız haftalarca sürecek hata ayıklama süreçlerinden kurtulursunuz.

    iOS Kod İmzalama

    İki temel yaklaşım vardır:

    Manuel yaklaşım: `.p12` sertifikanızı ve provisioning profile'ınızı dışa aktarın, base64 ile kodlayın, CI secret'ları olarak saklayın ve build sırasında decode edin. Yukarıdaki GitHub Actions workflow'u bu yöntemi göstermektedir.

    Fastlane Match: Match, sertifikalarınızı ve profile'larınızı şifrelenmiş bir Git deposunda saklar. Her ekip üyesi ve CI runner aynı kaynaktan çeker. "Benim makinemde çalışıyor" tarzı imzalama sorunları ortadan kalkar.

    bash
    # Match'i ilk kez başlat
    fastlane match init
    
    # Yeni sertifika oluştur
    fastlane match appstore
    fastlane match development

    Dikkat edilmesi gerekenler:

  • Apple, dağıtım sertifikası sayısını sınırlar. Her geliştirici için yeni sertifika oluşturmayın.
  • Provisioning profile'lar yıllık olarak sona erer. Yenileme hatırlatıcısını sürecinize dahil edin.
  • İmzalama bilgilerini asla kaynak kodunuzda saklamayın. Her zaman şifrelenmiş secret'lar veya özel bir secret yöneticisi kullanın.
  • Android Kod İmzalama

    Android imzalama daha basittir ama yine de dikkat gerektirir:

  • Keystore'u bir kez oluşturun ve güvenli bir yerde saklayın. Kaybetmek, uygulamanızı güncelleyemeyeceğiniz anlamına gelir.
  • Keystore'u base64 ile kodlayarak CI secret olarak saklayın.
  • `android/app/build.gradle` dosyasında `key.properties` üzerinden referans verin.
  • groovy
    "code-comment">// android/app/build.gradle
    def keystoreProperties = new Properties()
    def keystorePropertiesFile = rootProject.file('key.properties')
    if (keystorePropertiesFile.exists()) {
        keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
    }
    
    android {
        signingConfigs {
            release {
                keyAlias keystoreProperties['keyAlias']
                keyPassword keystoreProperties['keyPassword']
                storeFile file(keystoreProperties['storeFile'])
                storePassword keystoreProperties['storePassword']
            }
        }
        buildTypes {
            release {
                signingConfig signingConfigs.release
            }
        }
    }

    Kendi release pipeline'larımda Android keystore'unun yedeğini her zaman ayrı ve şifrelenmiş bir konumda saklarım. Keystore kaybı kalıcıdır — Google bunu kurtarmanıza izin vermez.

    Release Stratejisi

    Kod yayınlamak işin yalnızca yarısıdır. Kontrol, görünürlük ve geri alma planı sunan bir stratejiye ihtiyacınız var.

    Semantik Sürümleme

    Semantik sürümlemeyi (`MAJOR.MINOR.PATCH`) benimseyin ve iş akışınızın bir parçası yapın:

  • **MAJOR**: Kırılgan değişiklikler, büyük yeniden tasarımlar
  • **MINOR**: Yeni özellikler, geriye dönük uyumlu
  • **PATCH**: Hata düzeltmeleri, küçük iyileştirmeler
  • Flutter'da sürüm bilgisi `pubspec.yaml` içinde yer alır:

    yaml
    version: 2.4.1+42
    #        ^ semver  ^ build numarası

    `+` işaretinden sonraki build numarası her mağaza gönderiminde artmalıdır. CI'da bunu pipeline çalışma numarasını kullanarak otomatikleştiriyorum:

    bash
    flutter build appbundle --build-number=$GITHUB_RUN_NUMBER

    Otomatik Changelog Oluşturma

    Changelog'ları commit mesajlarınızdan otomatik oluşturun. Bu, commit kurallarında disiplin gerektirir:

    bash
    # conventional-changelog kurulumu
    npm install -g conventional-changelog-cli
    
    # CHANGELOG.md oluştur
    conventional-changelog -p angular -i CHANGELOG.md -s

    Ekibiniz conventional commit formatını (`feat:`, `fix:`, `chore:`) kullanıyorsa bu süreç tamamen otomatik hale gelir.

    Beta Dağıtımı

    Asla doğrudan production'a yayınlamayın. Kademeli bir dağıtım stratejisi izleyin:

  • **Dahili kanal** — Ekip ve QA build'i test eder.
  • **Kapalı beta** — Seçilmiş harici kullanıcılar geri bildirim verir.
  • **Açık beta / Kademeli dağıtım** — Kullanıcıların belirli bir yüzdesine kademeli dağıtım.
  • **Tam production** — Herkese yayınlama.
  • iOS için TestFlight beta kanalınız olarak hizmet verir. Android için Google Play'in dahili ve kapalı test kanallarını kullanın. Firebase App Distribution, mağaza inceleme gecikmesi olmadan daha hızlı geri bildirim döngüleri istediğinizde her iki platform için de mükemmel bir alternatiftir.

    Geri Alma Planı

    Her release'in bir kaçış yolu olmalıdır. Pratikte bu şu anlama gelir:

  • Önceki sürümün artifact'ini yeniden dağıtım için hazır tutun.
  • Yeni bir binary göndermeden yeni işlevselliği devre dışı bırakmak için feature flag'ler kullanın.
  • Android'de kademeli dağıtımlar durdurulup geri alınabilir. iOS'ta "satıştan kaldır" seçeneği vardır ama kolayca geri alınamaz, bu da kapsamlı TestFlight testini kritik kılar.
  • Sık Yapılan CI/CD Hataları

    Bu hataları defalarca gördüm — hem kendi projelerimde hem de çalıştığım ekiplerde.

    1. Bağımlılıkları Cache'lememek

    Flutter; Dart paketlerini, Gradle bağımlılıklarını, CocoaPods'u ve bazen Flutter SDK'nın kendisini bile indirir. Cache olmadan her build 5-15 dakikayı indirmelere harcar.

    yaml
    # GitHub Actions cache örneği
    - uses: subosito/flutter-action@v2
      with:
        flutter-version: "3.24.0"
        cache: true  # Flutter SDK'yı cache'ler
    
    - uses: actions/cache@v4
      with:
        path: |
          ~/.pub-cache
          ~/.gradle/caches
          ios/Pods
        key: ${{ runner.os }}-deps-${{ hashFiles('**/pubspec.lock', '**/Podfile.lock') }}

    2. iOS Build'lerini Linux Runner'da Çalıştırmak

    iOS build'leri macOS gerektirir. Bu kulağa bariz gelebilir ama `flutter build ipa` komutunu Ubuntu runner'da çalıştırıp sessizce başarısız olan pipeline'lar gördüm. iOS job'ları için her zaman `runs-on: macos-latest` kullanın.

    3. Secret'ları Depoya Gömmek

    Keystore'lar, API anahtarları, servis hesabı JSON dosyaları — bunların hiçbiri Git deponuzda olmamalıdır, `.gitignore` ile hariç tutulan bir dizinde bile olsa. CI sağlayıcınızın şifrelenmiş secret özelliğini veya HashiCorp Vault gibi özel bir secret yöneticisini kullanın.

    4. Pipeline'ı "Hızlandırmak" İçin Testleri Atlamak

    Testleri "çok uzun sürüyor" diye devre dışı bırakmak CI'ın amacını yok eder. Testleriniz yavaşsa onları optimize edin — mock kullanın, testleri paralel çalıştırın, entegrasyon testlerini ayrı bir aşamaya taşıyın. Atlamayın.

    5. Build Numarası Stratejisi Olmaması

    Build numaranız tutarlı bir şekilde artmıyorsa mağaza yüklemeleri reddedilir. Build numarasını artması garanti olan bir değerden türetin: CI çalışma numarası, zaman damgası veya sürüm kontrolündeki bir sayaç.

    6. Kararsız Testleri Görmezden Gelmek

    Görmezden gelinen kararsız bir test, hiç test olmamasından daha kötüdür. Ekibi CI hatalarını önemsememeye alıştırır. Kararsız testleri hemen düzeltin veya silin.

    7. Build ve Deploy Aşamalarını Ayırmamak

    Build ve deploy ayrı pipeline aşamaları olmalıdır. Deploy adımınız başarısız olursa aynı artifact'i yeniden build etmeden deploy edebilmelisiniz. Build adımında artifact'inizi yükleyin, deploy adımında indirin.

    Her Şeyi Bir Araya Getirmek

    Olgun bir Flutter CI/CD pipeline şöyle görünür:

  • Geliştirici feature branch'e push yapar.
  • CI, her push'ta format kontrolü, statik analiz ve testleri çalıştırır.
  • PR `develop`'a merge edilir — CI build'i oluşturur ve dahili teste deploy eder.
  • Release branch oluşturulur ve tag'lenir (örn. `v2.4.1`).
  • CI her iki platformu build eder, artifact'leri imzalar, changelog oluşturur.
  • Artifact'ler TestFlight ve Google Play dahili kanalına yüklenir.
  • QA onayından sonra, bir promotion job'ı build'i production'a taşır.
  • Tek manuel adımlar PR'ı merge etmek, tag'i oluşturmak ve promotion'ı onaylamaktır. Geri kalan her şey otomatiktir.

    Sonuç

    CI/CD, Flutter projeleri için "olsa iyi olur" değil, olmazsa olmazdır. Pipeline yapılandırmasına yapılan ön yatırım ilk birkaç release'te kendini amorti eder. Daha hızlı yayınlarsınız, daha az hata yaparsınız ve her release'in aynı titiz, otomatik süreçten geçtiğini bilerek daha rahat uyursunuz.

    İsterseniz Flutter projenize ve ekip iş akışınıza özel bir CI/CD pipeline tasarlayabilirim.

    İlgili Makaleler

    Flutter Projeniz mi Var?

    iOS, Android ve web için yüksek performanslı Flutter uygulamaları geliştiriyorum.

    İletişime Geç