Her push'ta terminalden flutter build ipa çalıştırıp App Store Connect'e elle yüklemek, tek başına çalışırken sorun çıkarmaz; ama ekip büyüdüğü ve platform sayısı arttığı anda sürdürülemez hale gelir. GitHub Actions ve Fastlane ikilisiyle kurulan bir Flutter CI CD GitHub Actions pipeline'ı, kod her push'landığında analiz-test-build-deploy adımlarını otomatik yürütür ve insan hatasını süreçten çıkarır. Bu rehberde bu pipeline'ı sıfırdan, macOS runner maliyetini gözeterek kuruyoruz.
💡 Pro Tip: Deploy job'ını yalnızca main branch'e push'larda tetikle; her PR'da TestFlight'a yükleme yapmak hem dakika bütçeni hem de test cihazlarındaki tester'ların bildirim kutusunu gereksiz yere şişirir.İçindekiler
- Pipeline'ın 4 Aşaması: Analyze, Test, Build, Deploy
- GitHub Actions Workflow İskeleti
- Tetikleyiciler (on)
- Job'lar Arası Bağımlılık (needs)
- macOS Runner Maliyeti ve Cache Stratejisi
- Fastlane match ile Sertifika ve Profile Yönetimi
- Matchfile Oluşturma
- CI Lane'inde Kullanım
- TestFlight ve Play Internal Testing Yüklemesi
- iOS: pilot ile TestFlight
- Android: supply ile Play Internal Testing
- Sürüm Numarası ve Build Number Otomasyonu
- Secret Yönetimi ve Güvenlik
- Flavor'lara Göre Çoklu Build
- Fastfile'da Flavor Parametresi
- Matrix Build ile Paralel Flavor'lar
- Yaygın 5 CI Hatası
- SSS
- Flutter uygulaması GitHub Actions ile nasıl derlenir?
- Flutter'da TestFlight'a otomatik yükleme nasıl yapılır?
- CI'da iOS imzalama sertifikaları nasıl yönetilir?
- Flutter için ücretsiz CI seçenekleri neler?
- Android tarafında internal testing yüklemesi nasıl otomatikleştirilir?
- Güncelleme (Eylül 2026)
- Sonuç
- Kaynaklar
Pipeline'ın 4 Aşaması: Analyze, Test, Build, Deploy
Bir Flutter CI/CD pipeline'ını tek dev job'a sıkıştırmak yerine dört ayrı adıma bölmek, hem hatayı erken yakalar hem de pahalı macOS runner dakikalarını israf etmez. Flutter'ın resmi continuous delivery rehberi de dağıtımı fastlane üzerinden ayrı aşamalar halinde kurmayı önerir.
Aşama | Komut / Araç | Amaç |
|---|---|---|
Analyze | flutter analyze | Statik analiz, lint hataları — saniyeler sürer, Linux runner'da çalışabilir |
Test | flutter test | Unit + widget testleri — Linux runner'da çalışır, macOS gerektirmez |
Build | flutter build ipa / flutter build appbundle | Platform binary'lerini üretir — iOS için macOS runner şart |
Deploy | fastlane pilot / supply | TestFlight ve Play Internal Testing'e yükleme |
Kritik nokta: yalnızca build ve deploy adımları macOS runner'a ihtiyaç duyar. Analyze ve test adımlarını ucuz ubuntu-latest runner'da çalıştırmak, dakika faturanı doğrudan düşürür.
GitHub Actions Workflow İskeleti
GitHub Actions'ta bir workflow, .github/workflows/ altındaki bir YAML dosyasıdır; name, on (tetikleyici) ve jobs alanlarından oluşur, her job da runs-on (çalışacağı runner) ve steps (adım listesi) içerir. Aşağıdaki iskelet, 4 aşamayı ayrı job'lara böler; test job'u Linux'ta, build/deploy job'ları macOS'ta çalışır:
yaml
1name: Flutter CI/CD2 3on:4 push:5 branches: [main]6 pull_request:7 branches: [main]8 9jobs:10 analyze_test:11 runs-on: ubuntu-latest12 steps:13 - uses: actions/checkout@v414 - uses: subosito/flutter-action@v215 with:16 flutter-version: "3.27.0"17 - run: flutter pub get18 - run: flutter analyze19 - run: flutter test20 21 build_deploy:22 needs: analyze_test23 if: github.ref == 'refs/heads/main'24 runs-on: macos-1425 steps:26 - uses: actions/checkout@v427 - uses: subosito/flutter-action@v228 with:29 flutter-version: "3.27.0"30 - run: flutter pub get31 - name: Install signing via fastlane match32 run: cd ios && bundle exec fastlane signing33 env:34 MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}35 - run: flutter build ipa --release36 - name: Deploy via fastlane37 run: cd ios && bundle exec fastlane release38 env:39 APP_STORE_CONNECT_API_KEY: ${{ secrets.APP_STORE_CONNECT_API_KEY }}subosito/flutter-action topluluk aksiyonu, runner'a belirli bir Flutter SDK sürümünü kurar; resmi GitHub Actions dokümantasyonu bir workflow'un name, on, jobs, runs-on ve steps alanlarından oluştuğunu ve uses: actions/checkout@v4 ile reponun kodunu runner'a çektiğini tarif eder.
Tetikleyiciler (on)
on bloğu, workflow'un ne zaman çalışacağını belirler. Yukarıdaki örnekte hem push hem pull_request tetikleyicisi var; ama build_deploy job'undaki if: github.ref == 'refs/heads/main' koşulu, PR'larda yalnızca analyze_test job'unun çalışmasını, build ve deploy adımlarının ise sadece main'e gerçek bir push geldiğinde tetiklenmesini sağlıyor. Bu ayrım, her PR açılışında gereksiz macOS runner dakikası harcamanın önüne geçiyor.
Job'lar Arası Bağımlılık (needs)
needs: analyze_test satırı, build_deploy job'unun analyze_test job'u başarıyla bitmeden başlamayacağını garanti eder. Analiz veya testler kırmızı çıkarsa build hiç tetiklenmez — bu da bozuk bir kod parçasının TestFlight'a kadar ilerlemesini daha en baştan engeller.
macOS Runner Maliyeti ve Cache Stratejisi
macOS runner'lar GitHub Actions'ta Linux runner'lardan belirgin şekilde daha pahalıdır; bu yüzden yukarıdaki iskelette analyze/test job'unu bilerek ubuntu-latest'e, yalnızca build/deploy'u macos-14'e verdik. Maliyeti daha da azaltmanın ikinci yolu cache'tir.
GitHub'ın resmi actions/cache dokümantasyonu, aksiyonun önce key ile tam eşleşme aradığını, bulamazsa restore-keys ile sırayla kısmi eşleşmeye baktığını belirtir. Flutter projelerinde pub cache'i şu şekilde önbelleğe alabilirsin:
yaml
1- uses: actions/cache@v42 with:3 path: |4 ~/.pub-cache5 **/.dart_tool6 key: ${{ runner.os }}-pub-${{ hashFiles('**/pubspec.lock') }}7 restore-keys: |8 ${{ runner.os }}-pub-pubspec.lock değişmediği sürece bu cache aynı kalır ve flutter pub get saniyeler içinde biter. iOS tarafında CocoaPods için de benzer şekilde ios/Pods dizinini Podfile.lock hash'ine göre cache'leyebilirsin — bu, macOS runner'ın en yavaş adımlarından birini (pod install) doğrudan kısaltır.
Fastlane match ile Sertifika ve Profile Yönetimi
CI'da iOS imzalama en çok baş ağrıtan kısımdır çünkü sertifika ve provisioning profile'lar normalde tek bir Mac'e bağlıdır. fastlane'in match aracı bunu çözer: resmi dokümantasyona göre match "gerekli tüm sertifikaları ve provisioning profile'ları oluşturur ve ayrı bir git deposunda, Google Cloud'da veya Amazon S3'te saklar", böylece ekip ve CI aynı imzalama kimliğini paylaşır.
Depolama | Nasıl çalışır | Ne zaman tercih edilir |
|---|---|---|
Git deposu | Sertifikalar OpenSSL ile şifrelenip private repoya push edilir | Küçük-orta ekip, ekstra bulut hesabı istemiyorsan |
Google Cloud | Google'ın yönettiği anahtarlarla şifrelenip GCS'de saklanır | Zaten GCP kullanan ekipler |
Amazon S3 | Kendi sağladığın bucket içinde saklanır | Zaten AWS kullanan ekipler |
Matchfile Oluşturma
fastlane match init çalıştırdığında bir Matchfile oluşur:
ruby
1git_url("https://github.com/<org>/certificates")2app_identifier("com.example.app")3username("[email protected]")CI Lane'inde Kullanım
match, imzalanmış build'i oluşturan adımdan önce çağrılmalıdır — dokümantasyon bunu açıkça "match, gym ile build almadan önce çağrılmalı" diye belirtir. Bu pipeline'da IPA'yı fastlane'in gym aksiyonu değil workflow'daki flutter build ipa adımı ürettiği için match ayrı bir signing lane'inde durur ve workflow'da build adımından önce çağrılır:
ruby
1lane :signing do2 match(type: "appstore", readonly: true)3endCI'da readonly: true kullanmak, pipeline'ın yanlışlıkla yeni sertifika üretmesini engeller — bu ayrım özellikle çoklu geliştirici + CI kombinasyonunda kritik.
TestFlight ve Play Internal Testing Yüklemesi
iOS: pilot ile TestFlight
Build imzalandıktan sonra sıra dağıtımda. fastlane'in pilot aksiyonu (diğer adıyla upload_to_testflight) build'i TestFlight'a yükler; resmi dokümantasyon API key yönteminin avantajları arasında 2FA gerekmemesini ve Apple ID'ye göre daha iyi performansı sayar:
ruby
1lane :release do2 pilot(3 ipa: Dir[File.expand_path("../../build/ios/ipa/*.ipa")].first,4 api_key_path: "./fastlane/api_key.json",5 skip_waiting_for_build_processing: true6 )7endipa parametresi yüklenecek dosyanın yolunu verir: lane yeniden build almaz, workflow'daki flutter build ipa adımının build/ios/ipa/ altına ürettiği IPA'yı yükler (Fastfile'daki Ruby kodu fastlane/ klasöründen çalıştığı için yol iki üst dizinden başlar). skip_waiting_for_build_processing: true parametresi, fastlane'in build'in Apple tarafında işlenmesini bekleyip beklememesini kontrol eder; CI job'unun build yüklendikten sonra dakikalarca askıda kalmasını istemiyorsan bu değeri true bırakman, job'un yüklemeyi tetikleyip hemen bitmesini sağlar — ancak bu durumda distribute_external çalışmaz, build otomatik olarak tester'lara dağıtılmaz.
Android: supply ile Play Internal Testing
Android tarafında karşılığı upload_to_play_store (diğer adıyla supply) aksiyonudur. Dokümantasyona göre varsayılan track seçenekleri production, beta, alpha, internal'dır — CI'dan her push'ta doğrudan production'a değil, internal track'e yüklemek doğru alışkanlıktır:
ruby
1platform :android do2 lane :release do3 upload_to_play_store(4 track: "internal",5 json_key: ENV["PLAY_STORE_JSON_KEY"]6 )7 end8endjson_key parametresi, Google Cloud servis hesabına ait kimlik dosyasının yolunu (veya CI'da ortam değişkeninden gelen içeriğini) gösterir. Bu iki lane'i aynı Fastfile'da platform :ios do ... end ve platform :android do ... end bloklarıyla ayırman, tek dosyadan iki platformu da yönetmeni sağlar.
Sürüm Numarası ve Build Number Otomasyonu
Flutter'da sürüm bilgisi pubspec.yaml içinde tek satırda tutulur; resmi dokümantasyona göre format {version}+{build-number} şeklindedir:
yaml
1version: 1.0.0+1Burada 1.0.0 kullanıcıya görünen sürümdür (iOS'ta CFBundleShortVersionString), 1 ise build numarasıdır (CFBundleVersion). CI'da her build'de bu değeri elle güncellemek yerine, build numarasını GitHub Actions'ın kendi run sayacından üretip komut satırından override edebilirsin — dokümantasyon bu override'ı açıkça destekler:
yaml
1- name: Build IPA2 run: |3 flutter build ipa \4 --build-name=1.2.0 \5 --build-number=${{ github.run_number }}github.run_number, o workflow'un o depoda kaçıncı kez çalıştığını veren, GitHub'ın otomatik sağladığı bir bağlam değişkenidir; her push'ta artan, tekilliği garanti eden bir build numarası ister. --build-name değerini ise genelde pubspec.yaml'daki gibi elle veya bir git tag'inden okuyarak veriyorsun — anlamlı sürüm numaralandırması (semver) hâlâ insan kararı gerektirir, otomatikleştirilen kısım yalnızca build numarasıdır.
Secret Yönetimi ve Güvenlik
MATCH_PASSWORD, App Store Connect API key, Play Store service account JSON'ı — bunların hiçbiri repoya commit edilmemeli. GitHub'ın resmi rehberi, secret eklemeyi Settings → Secrets and variables → Actions → New repository secret üzerinden veya CLI ile tarif eder:
bash
1gh secret set MATCH_PASSWORDWorkflow YAML'ında secret'a secrets context'i üzerinden erişilir:
yaml
1env:2 MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}Kritik bir güvenlik detayı: GitHub'ın dokümantasyonu, fork'tan gelen pull request'lerde GITHUB_TOKEN dışındaki secret'ların runner'a hiç geçirilmediğini açıkça belirtir. Bu, dışarıdan biri PR açıp workflow dosyasını değiştirerek secret'ları console'a yazdırmaya çalışsa bile başarısız olacağı anlamına gelir — ama bu koruma yalnızca fork PR'ları için geçerlidir, aynı repo içindeki branch'lerden açılan PR'lar secret'lara erişebilir. Flutter'ın kendi CI rehberi de test script'lerinde secret değerlerini console'a "re-echo" etmemen gerektiğini vurgular; set -x gibi debug modlarını fastlane lane'lerinde açık bırakmak, log'lara secret sızdırmanın en sık nedenidir.
Flavor'lara Göre Çoklu Build
Fastfile'da Flavor Parametresi
Dev/staging/prod flavor'ların her biri farklı bundle ID, farklı imzalama ve farklı App Store Connect/Play Console kaydı gerektirir. Bunu tek Fastfile'da yönetmenin yolu, flavor adını parametre olarak geçirmektir. flutter build ipa komutuna verdiğin --flavor bayrağı ve -t ile belirttiğin entry-point dosyası (lib/main_dev.dart, lib/main_prod.dart gibi), Flutter'ın flavor'lara özgü build almak için kullandığı standart yapıdır; her flavor kendi Info.plist/bundle ID ayarına Xcode scheme'i üzerinden bağlanır:
ruby
1lane :release do |options|2 flavor = options[:flavor] || "prod"3 match(type: "appstore", app_identifier: "com.example.app.#{flavor}", readonly: true)4 sh("flutter build ipa --flavor #{flavor} --release " \5 "-t lib/main_#{flavor}.dart")6 pilot(7 ipa: Dir[File.expand_path("../../build/ios/ipa/*.ipa")].first,8 api_key_path: "./fastlane/api_key.json"9 )10endMatrix Build ile Paralel Flavor'lar
GitHub Actions tarafında bunu matrix build ile her flavor için ayrı job çalıştırarak tetikleyebilirsin:
yaml
1strategy:2 matrix:3 flavor: [dev, staging, prod]4steps:5 - run: bundle exec fastlane release flavor:${{ matrix.flavor }}Bu yapı, üç flavor'ı tek workflow dosyasında, paralel job'lar olarak çalıştırır — her biri kendi match app_identifier'ıyla kendi sertifikasını çeker, birbirine karışmaz. Flavor sayısı arttıkça matrix'e yeni satır eklemek, Fastfile'ı kopyalamaktan çok daha az bakım yükü getirir.
Yaygın 5 CI Hatası
ALTIN İPUCU
Bu yazının en değerli bilgisi
Bu ipucu, yazının en önemli çıkarımını içeriyor.
Easter Egg
Gizli bir bilgi buldun!
Bu bölümde gizli bir bilgi var. Keşfetmek ister misin?
Okuyucu Ödülü
Bu rehberi baştan sona uyguladıysan, kendi projende bir Flutter CI/CD pipeline'ı kurmuş olmalısın. Aşağıdaki checklist, projene entegre etmeden önce gözden geçirmen gereken adımları, makalede geçen sırayla özetliyor — her maddeyi işaretleyerek eksik bıraktığın bir adım olup olmadığını kontrol edebilirsin.
- match'i readonly olmadan çalıştırmak: CI'da
readonly: truevermezsen, her build yeni sertifika üretmeye çalışabilir ve Apple'ın sertifika limitine takılırsın; sertifikaları yalnızca lokalde, elle üretip CI'ya salt-okunur bıraktır. - Secret'ları workflow dosyasına düz metin yazmak:
env: MATCH_PASSWORD: "gercek-sifre"gibi bir satır, repo geçmişine kalıcı olarak işler; her zamansecretscontext'i kullan. - Test ve build'i tek job'da birleştirmek: Bir widget testi flaky çıktığında bütün macOS runner dakikan (build dahil) boşa gider; test'i ayrı ve ucuz bir runner'da tut.
- Build numarasını elle güncellemeyi unutmak:
flutter build ipaher seferinde aynı build number ile giderse App Store Connect yüklemeyi reddeder; CI'da bunu otomatikleştirmek (bkz. yukarıdaki bölüm) bu hatayı kalıcı olarak ortadan kaldırır. - Cache key'ini yanlış kurmak:
key'i statik bir string yaparsan (örn. sadecepub-cache),pubspec.lockdeğişse bile eski bağımlılıklar servis edilir; key'i her zamanhashFiles('**/pubspec.lock')gibi içerik-duyarlı bir değere bağla.
SSS
Flutter uygulaması GitHub Actions ile nasıl derlenir?
Reponun .github/workflows/ klasörüne bir YAML dosyası eklersin; içinde subosito/flutter-action gibi bir topluluk aksiyonuyla Flutter SDK'sını kurar, ardından flutter pub get ve platforma göre flutter build appbundle (Android) veya flutter build ipa (iOS, macOS runner gerektirir) komutlarını çalıştırırsın. Bu makaledeki GitHub Actions Workflow İskeleti bölümünde tam bir örnek var.
Flutter'da TestFlight'a otomatik yükleme nasıl yapılır?
fastlane'in pilot aksiyonu (upload_to_testflight) bunu yapar. IPA'yı imzalayıp ürettikten (match + flutter build ipa) sonra Fastfile'ında pilot(ipa: "...", api_key_path: "...") çağırman yeterli; App Store Connect API key kullanmak 2FA gerekliliğini ortadan kaldırır.
CI'da iOS imzalama sertifikaları nasıl yönetilir?
fastlane match ile. Sertifikalar ve provisioning profile'lar şifrelenip bir git deposunda, Google Cloud'da veya S3'te saklanır; CI, match(readonly: true) çağrısıyla bu sertifikaları salt-okunur şekilde çeker, yeni sertifika üretmez.
Flutter için ücretsiz CI seçenekleri neler?
GitHub Actions, public repolar için sınırsız, private repolar için aylık belirli bir dakika kotasıyla ücretsizdir; ancak macOS runner dakikaları Linux runner'lara göre kat kat daha yüksek bir çarpanla kotadan düşer, bu yüzden bu makaledeki gibi build/deploy adımlarını yalnızca gerektiğinde macOS'a taşımak bütçeyi korur.
Android tarafında internal testing yüklemesi nasıl otomatikleştirilir?
fastlane'in upload_to_play_store (supply) aksiyonuyla; track: "internal" parametresi build'i doğrudan production yerine Play Console'daki Internal Testing track'ine yükler, json_key parametresiyle Google Cloud servis hesabı kimlik bilgisini geçirirsin.
Güncelleme (Eylül 2026)
Bu rehber Şubat 2025'te yazıldı; o tarihten bu yana Apple'ın App Store Connect yükleme gereksinimlerinde iki değişiklik pipeline'ını doğrudan etkiliyor. Apple'ın resmi "Upcoming requirements" sayfasına göre, 28 Nisan 2026'dan itibaren App Store Connect'e yüklenen uygulamaların Xcode 26 veya üzeri ile, iOS 26/iPadOS 26/tvOS 26/visionOS 26/watchOS 26 SDK'sı kullanılarak derlenmiş olması zorunlu hale geldi. Bu, macos-14 gibi eski runner image'ları kullanan workflow'ların artık uygun Xcode sürümünü barındıran daha güncel bir macOS runner'a (örneğin macos-15 veya sonrası) geçmesi gerektiği anlamına geliyor; GitHub'ın runner-images deposundaki image güncellemelerini takip etmek bu geçişte referans noktan olmalı.
Aynı sayfa, 9 Eylül 2026'dan itibaren App Store Connect'e yüklenen iOS/iPadOS uygulamalarının en az iOS 13'ü hedeflemesi gerektiğini de netleştirdi — yani deployment target'ını bunun altında tutamazsın. Projenin Xcode ayarlarındaki iOS Deployment Target alanını ve Flutter'ın kendi desteklediği minimum sürümü (resmi Supported deployment platforms sayfasından kontrol edebilirsin) bu iki gereksinimle uyumlu tutman gerekiyor. Fastfile veya CI workflow'unda başka bir değişiklik gerekmiyor; asıl etkilenen, build job'unun çalıştığı runner image'ı ve Xcode sürümü.
Sonradan yayımlanan ilgili yazılar:
- Flutter Riverpod ile State Management
- Flutter Clean Architecture
- Flutter Firebase Entegrasyonu
- Flutter Performans Optimizasyonu
Sonuç
Flutter CI/CD GitHub Actions ve Fastlane ikilisiyle kurulan bir pipeline, dört basit prensibe dayanıyor: aşamaları ayır (analyze/test ucuz runner'da, build/deploy macOS'ta), sertifikaları match ile paylaş, secret'ları hiçbir zaman repoya yazma, build numarasını otomatikleştir. Bu dört prensibi uyguladığında elle build alıp yükleme sürecinin tamamı ortadan kalkıyor ve her push, kendi kendine test edilip dağıtılan bir sürüm adayına dönüşüyor.
Native iOS tarafında aynı ikilinin nasıl kurulduğunu görmek istersen iOS CI/CD Pipeline: GitHub Actions ve Fastlane rehberine bakabilirsin — orada Swift/Xcode projesi özelinde aynı match+gym+pilot akışı işleniyor; bu makale ise Flutter'ın iki platformu (iOS+Android) tek Fastfile'dan yönetme kısmına odaklanıyor.
React Native'den geçiş düşünüyorsan React Native vs Flutter Karşılaştırması karar vermene yardımcı olabilir.
Kaynaklar
- Continuous delivery with Flutter — Flutter Docs — fastlane kurulumu, secret yönetimi, GitHub Actions/Codemagic/Xcode Cloud seçenekleri.
- Build and release an iOS app — Flutter Docs — Bundle ID kaydı, pubspec.yaml sürüm formatı,
flutter build ipave TestFlight/App Store yükleme adımları. - iOS setup — fastlane Docs — fastlane kurulumu,
fastlane init, Fastfile yapısı. - match — fastlane Docs — sertifika/profile paylaşımı, depolama seçenekleri, Matchfile.
- pilot — fastlane Docs — TestFlight'a otomatik yükleme, App Store Connect API key kullanımı.
- upload_to_play_store — fastlane Docs — Play Store track seçenekleri, json_key kullanımı.
- Caching dependencies to speed up workflows — GitHub Docs —
actions/cache, key/restore-keys mantığı. - Using secrets in GitHub Actions — GitHub Docs — secret ekleme, fork PR'larda secret erişim kısıtı.
- SDK minimum requirements — Apple Developer — Xcode/iOS SDK zorunluluk tarihleri (Nisan 2026, Eylül 2026).

