Flutter

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

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

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:

  1. Kod kalite kontrolleri — lint, format, statik analiz
  2. Otomatik testler — unit, widget ve entegrasyon testleri
  3. Build ve artifact oluşturma — platforma özel binary'ler
  4. 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:

  1. Keystore'u bir kez oluşturun ve güvenli bir yerde saklayın. Kaybetmek, uygulamanızı güncelleyemeyeceğiniz anlamına gelir.
  2. Keystore'u base64 ile kodlayarak CI secret olarak saklayın.
  3. android/app/build.gradle dosyasında key.properties üzerinden referans verin.
groovy
// 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:

  1. Dahili kanal — Ekip ve QA build'i test eder.
  2. Kapalı beta — Seçilmiş harici kullanıcılar geri bildirim verir.
  3. Açık beta / Kademeli dağıtım — Kullanıcıların belirli bir yüzdesine kademeli dağıtım.
  4. 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:

  1. Geliştirici feature branch'e push yapar.
  2. CI, her push'ta format kontrolü, statik analiz ve testleri çalıştırır.
  3. PR develop'a merge edilir — CI build'i oluşturur ve dahili teste deploy eder.
  4. Release branch oluşturulur ve tag'lenir (örn. v2.4.1).
  5. CI her iki platformu build eder, artifact'leri imzalar, changelog oluşturur.
  6. Artifact'ler TestFlight ve Google Play dahili kanalına yüklenir.
  7. 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ç