Flutter'da CI/CD: GitHub Actions ile Otomatik Build ve Dağıtım

Her push veya pull request'te testlerin çalışması, APK/IPA oluşturulması ve dağıtımın otomatik yapılması; hataları erken yakalar ve deployment sürecini güvenilir kılar. Bu yazıda GitHub Actions ile Flutter için eksiksiz bir CI/CD pipeline kuracağız.

Temel Workflow: Test ve Analiz

# .github/workflows/ci.yml
name: CI

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  test:
    name: Test & Analyze
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Flutter Setup
        uses: subosito/flutter-action@v2
        with:
          flutter-version: '3.24.x'
          channel: stable
          cache: true

      - name: Cache pub packages
        uses: actions/cache@v3
        with:
          path: ~/.pub-cache
          key: ${{ runner.os }}-pub-${{ hashFiles('**/pubspec.lock') }}
          restore-keys: ${{ runner.os }}-pub-

      - name: Install dependencies
        run: flutter pub get

      - name: Generate code
        run: dart run build_runner build --delete-conflicting-outputs

      - name: Analyze
        run: flutter analyze --no-fatal-infos

      - name: Format check
        run: dart format --set-exit-if-changed lib/ test/

      - name: Run tests
        run: flutter test --coverage --reporter=github

      - name: Upload coverage
        uses: codecov/codecov-action@v3
        with:
          file: coverage/lcov.info

Android Build ve İmzalama

# .github/workflows/android.yml
name: Android Build

on:
  push:
    branches: [main]
  workflow_dispatch:

jobs:
  build-android:
    name: Build Android
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-java@v4
        with:
          distribution: temurin
          java-version: '17'

      - uses: subosito/flutter-action@v2
        with:
          flutter-version: '3.24.x'
          channel: stable
          cache: true

      - name: Flutter pub get & codegen
        run: |
          flutter pub get
          dart run build_runner build --delete-conflicting-outputs

      # Keystore dosyasını Secret'tan oluştur
      - name: Decode Keystore
        run: |
          echo "${{ secrets.ANDROID_KEYSTORE_BASE64 }}" | base64 --decode > android/app/release.keystore

      # key.properties dosyası oluştur
      - name: Create key.properties
        run: |
          cat > android/key.properties <<EOF
          storePassword=${{ secrets.ANDROID_STORE_PASSWORD }}
          keyPassword=${{ secrets.ANDROID_KEY_PASSWORD }}
          keyAlias=${{ secrets.ANDROID_KEY_ALIAS }}
          storeFile=release.keystore
          EOF

      # .env dosyası oluştur (API keys vb.)
      - name: Create .env
        run: |
          echo "API_URL=${{ secrets.API_URL }}" > .env
          echo "GOOGLE_MAPS_KEY=${{ secrets.GOOGLE_MAPS_KEY }}" >> .env

      - name: Build APK (debug)
        run: flutter build apk --debug --flavor dev -t lib/main_dev.dart

      - name: Build App Bundle (release)
        run: |
          flutter build appbundle \
            --release \
            --flavor prod \
            -t lib/main_prod.dart \
            --dart-define=FLAVOR=prod

      - name: Upload APK Artifact
        uses: actions/upload-artifact@v4
        with:
          name: app-debug-apk
          path: build/app/outputs/flutter-apk/app-debug.apk

      # Firebase App Distribution'a yükle
      - name: Upload to Firebase App Distribution
        uses: wzieba/Firebase-Distribution-Github-Action@v1
        with:
          appId: ${{ secrets.FIREBASE_APP_ID_ANDROID }}
          serviceCredentialsFileContent: ${{ secrets.FIREBASE_SERVICE_ACCOUNT }}
          groups: qa-team
          file: build/app/outputs/flutter-apk/app-debug.apk
          releaseNotes: |
            Build: ${{ github.run_number }}
            Commit: ${{ github.sha }}
            Branch: ${{ github.ref_name }}

iOS Build ve İmzalama

# .github/workflows/ios.yml (kısmen)
  build-ios:
    name: Build iOS
    runs-on: macos-latest
    steps:
      - uses: actions/checkout@v4

      - uses: subosito/flutter-action@v2
        with:
          flutter-version: '3.24.x'
          channel: stable

      # Sertifikaları kur
      - name: Install Apple Certificate
        uses: apple-actions/import-codesign-certs@v2
        with:
          p12-file-base64: ${{ secrets.IOS_P12_BASE64 }}
          p12-password: ${{ secrets.IOS_P12_PASSWORD }}

      # Provisioning profile kur
      - name: Install Provisioning Profile
        run: |
          mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
          echo "${{ secrets.IOS_PROVISIONING_PROFILE_BASE64 }}" | base64 --decode \
            > ~/Library/MobileDevice/Provisioning\ Profiles/profile.mobileprovision

      - name: Flutter build iOS
        run: |
          flutter pub get
          dart run build_runner build --delete-conflicting-outputs
          flutter build ipa \
            --release \
            --export-options-plist ios/ExportOptions.plist

      # TestFlight'a yükle
      - name: Upload to TestFlight
        uses: apple-actions/upload-testflight-build@v1
        with:
          app-path: build/ios/ipa/*.ipa
          issuer-id: ${{ secrets.APPSTORE_ISSUER_ID }}
          api-key-id: ${{ secrets.APPSTORE_KEY_ID }}
          api-private-key: ${{ secrets.APPSTORE_PRIVATE_KEY }}

Environment ve Flavor Yönetimi

// lib/core/env.dart
enum AppFlavor { dev, staging, prod }

class Env {
  static late AppFlavor flavor;
  static late String apiUrl;
  static late String googleMapsKey;

  static void initialize() {
    const flavorStr = String.fromEnvironment('FLAVOR', defaultValue: 'dev');
    flavor = AppFlavor.values.byName(flavorStr);

    switch (flavor) {
      case AppFlavor.dev:
        apiUrl = 'https://dev.api.example.com';
        googleMapsKey = const String.fromEnvironment('GOOGLE_MAPS_KEY');
      case AppFlavor.staging:
        apiUrl = 'https://staging.api.example.com';
        googleMapsKey = const String.fromEnvironment('GOOGLE_MAPS_KEY');
      case AppFlavor.prod:
        apiUrl = 'https://api.example.com';
        googleMapsKey = const String.fromEnvironment('GOOGLE_MAPS_KEY');
    }
  }
}

// main_dev.dart
void main() {
  Env.initialize();
  runApp(const ProviderScope(child: MyApp()));
}

// Çalıştırma:
// flutter run -t lib/main_dev.dart --dart-define=FLAVOR=dev
// flutter build apk -t lib/main_prod.dart --dart-define=FLAVOR=prod

Semantic Versioning ve Git Tag ile Sürüm Yönetimi

# pubspec.yaml versiyonunu otomatik güncelle
# .github/workflows/release.yml
  - name: Bump version
    run: |
      VERSION=$(cat VERSION)
      BUILD=${{ github.run_number }}
      sed -i "s/^version:.*/version: $VERSION+$BUILD/" pubspec.yaml

  - name: Commit version bump
    run: |
      git config user.email "ci@example.com"
      git config user.name "CI Bot"
      git add pubspec.yaml
      git commit -m "chore: bump version to $VERSION+$BUILD [skip ci]"
      git push

GitHub Actions, Flutter CI/CD için ücretsiz ve yeterince güçlüdür. Android için keystore'u, iOS için p12 sertifikasını ve provisioning profile'ı GitHub Secrets'a ekleyin. Flavor sistemi ile dev/staging/prod ortamlarını ayırın. Firebase App Distribution, QA ekibine hızlı dağıtım için mükemmeldir.