Flutter CI/CD: Automatisierte Build-, Test- und Release-Pipeline

13 Min. Lesezeit9. Februar 2026Aktualisiert: 9. März 2026
Flutter CI/CDFlutter GitHub ActionsFlutter CodemagicFlutter FastlaneFlutter release automationFlutter build pipelineFlutter deploymentmobile CI/CD

# Flutter CI/CD: Automatisierte Build-, Test- und Release-Pipeline

Eine Flutter-App manuell zu veröffentlichen ist fehleranfällig, langsam und stressig. Eine gut durchdachte CI/CD-Pipeline beseitigt den menschlichen Engpass im Release-Prozess und lässt Sie sich auf das Schreiben von Code konzentrieren, statt Builds zu beaufsichtigen. In diesem Artikel gehe ich die gesamte Pipeline durch, die ich in der Produktion einsetze — vom Linting bis zur Store-Veröffentlichung — mit echten Konfigurationsbeispielen, die Sie sofort übernehmen können.

Warum CI/CD für Flutter wichtig ist

Flutter adressiert mehrere Plattformen aus einer einzigen Codebasis. Das bedeutet, dass jedes Release potenziell ein Android-APK/AAB, ein iOS-IPA, einen Web-Build und Desktop-Artefakte umfasst. Das manuell zu erledigen ist nicht nur mühsam — es ist ein Zuverlässigkeitsrisiko. Eine CI/CD-Pipeline bietet Ihnen:

  • **Konsistenz**: Jeder Build durchläuft dieselben Schritte in derselben Reihenfolge.
  • **Geschwindigkeit**: Parallele Jobs verkürzen Ihre Feedback-Schleife von Stunden auf Minuten.
  • **Vertrauen**: Automatisierte Tests fangen Regressionen ab, bevor sie bei den Nutzern ankommen.
  • **Nachvollziehbarkeit**: Jedes Artefakt ist einem bestimmten Commit zugeordnet.
  • Grundlegende Pipeline-Stufen

    Eine solide Flutter-CI/CD-Pipeline besteht aus vier Stufen:

  • **Code-Qualitätsprüfungen** — Lint, Format, statische Analyse
  • **Automatisierte Tests** — Unit-, Widget- und Integrationstests
  • **Build und Artefakt-Erzeugung** — plattformspezifische Binaries
  • **Release und Distribution** — Store-Uploads, Beta-Kanäle, Changelogs
  • Jede Stufe fungiert als Tor. Wenn ein Schritt fehlschlägt, stoppt die Pipeline und das Team wird sofort benachrichtigt.

    GitHub Actions Workflow

    GitHub Actions ist mein bevorzugtes Werkzeug für Flutter-CI/CD. Hier ist ein produktionsreifer Workflow, der Analyse, Tests und Builds für beide Plattformen abdeckt:

    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: Analyse & Test
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v4
    
          - uses: subosito/flutter-action@v2
            with:
              flutter-version: ${{ env.FLUTTER_VERSION }}
              cache: true
    
          - name: Abhängigkeiten installieren
            run: flutter pub get
    
          - name: Formatierung prüfen
            run: dart format --set-exit-if-changed .
    
          - name: Analyse ausführen
            run: flutter analyze --fatal-infos
    
          - name: Tests mit Coverage ausführen
            run: flutter test --coverage
    
          - name: Coverage hochladen
            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 dekodieren
            env:
              KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
            run: echo "$KEYSTORE_BASE64" | base64 --decode > android/app/release.keystore
    
          - name: key.properties erstellen
            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 erstellen
            run: flutter build appbundle --release
    
          - name: Artefakt hochladen
            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 installieren
            run: cd ios && pod install
    
          - name: Signierzertifikat importieren
            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 installieren
            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 erstellen
            run: flutter build ipa --release --export-options-plist=ios/ExportOptions.plist
    
          - name: Artefakt hochladen
            uses: actions/upload-artifact@v4
            with:
              name: ios-release
              path: build/ios/ipa/*.ipa

    In meinen Release-Pipelines pinne ich die Flutter-Version immer explizit fest. Sich auf "latest" zu verlassen, ist der sicherste Weg zu Überraschungsfehlern am Montagmorgen.

    Codemagic-Konfiguration

    Codemagic ist ein Flutter-spezialisierter CI/CD-Dienst, der Code-Signierung und Store-Veröffentlichung mit minimalem Aufwand ermöglicht. Hier ist eine `codemagic.yaml`-Konfiguration:

    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: Abhängigkeiten installieren
            script: flutter pub get
    
          - name: Analyse ausführen
            script: flutter analyze --fatal-infos
    
          - name: Tests ausführen
            script: flutter test
    
          - name: iOS-Code-Signierung einrichten
            script: |
              keychain initialize
              app-store-connect fetch-signing-files "$APP_ID" \
                --type IOS_APP_STORE \
                --create
              keychain add-certificates
    
          - name: iOS bauen
            script: |
              flutter build ipa --release \
                --build-number=$PROJECT_BUILD_NUMBER \
                --export-options-plist=/Users/builder/export_options.plist
    
          - name: Android bauen
            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

    Das Tag-basierte Triggering kann ich nur empfehlen. Das Pushen eines `v1.2.0`-Tags sollte der einzige manuelle Schritt in Ihrem Release-Prozess sein.

    Fastlane-Integration

    Fastlane übernimmt die "letzte Meile" der CI/CD — Code-Signierung, Screenshots, Metadaten und Store-Uploads. Selbst wenn Sie GitHub Actions oder Codemagic zum Bauen verwenden, ist Fastlane für die Verwaltung von Store-Einreichungen unverzichtbar.

    Android Fastfile

    ruby
    default_platform(:android)
    
    platform :android do
      desc "Auf Google Play internen Track deployen"
      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 "Von intern nach Produktion befördern"
      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 "Neues Release an TestFlight senden"
      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 "An den App Store senden"
      lane :release do
        deliver(
          submit_for_review: true,
          automatic_release: false,
          force: true,
          skip_metadata: false,
          skip_screenshots: true
        )
      end
    end

    In meinen Release-Pipelines verwende ich für die iOS-Code-Signierung immer `match`, weil es Zertifikate in einem privaten Git-Repository speichert, auf das das gesamte Team konsistent zugreifen kann.

    Code-Signierung für iOS und Android

    Die Code-Signierung ist der Punkt, an dem die meisten CI/CD-Setups scheitern. Von Anfang an richtig einzurichten spart Wochen an Debugging.

    iOS-Code-Signierung

    Es gibt zwei wesentliche Ansätze:

    Manueller Ansatz: Exportieren Sie Ihr `.p12`-Zertifikat und Provisioning Profile, kodieren Sie sie Base64, speichern Sie sie als CI-Secrets und dekodieren Sie sie während des Builds. Genau das zeigt der GitHub-Actions-Workflow oben.

    Fastlane Match: Match speichert Ihre Zertifikate und Profile in einem verschlüsselten Git-Repository. Jedes Teammitglied und jeder CI-Runner bezieht sie aus derselben Quelle. Keine "funktioniert auf meinem Rechner"-Signierungsprobleme mehr.

    bash
    # Match zum ersten Mal initialisieren
    fastlane match init
    
    # Neue Zertifikate generieren
    fastlane match appstore
    fastlane match development

    Wichtige Punkte:

  • Apple begrenzt die Anzahl der Distributionszertifikate. Erstellen Sie nicht für jeden Entwickler ein neues.
  • Provisioning Profiles laufen jährlich ab. Bauen Sie eine Erinnerung zur Erneuerung in Ihren Prozess ein.
  • Speichern Sie Signierungsdaten niemals in Ihrem Quell-Repository. Verwenden Sie immer verschlüsselte Secrets oder einen dedizierten Secrets-Manager.
  • Android-Code-Signierung

    Die Android-Signierung ist einfacher, erfordert aber dennoch Sorgfalt:

  • Erstellen Sie den Keystore einmalig und bewahren Sie ihn sicher auf. Ihn zu verlieren bedeutet, dass Sie Ihre App nicht mehr aktualisieren können.
  • Speichern Sie den Keystore als Base64-kodierten CI-Secret.
  • Referenzieren Sie ihn aus `android/app/build.gradle` über eine `key.properties`-Datei.
  • 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
            }
        }
    }

    In meinen Release-Pipelines speichere ich immer eine Sicherungskopie des Android-Keystores an einem separaten, verschlüsselten Ort. Der Verlust eines Keystores ist endgültig — Google bietet keine Möglichkeit zur Wiederherstellung.

    Release-Strategie

    Code auszuliefern ist nur die halbe Miete. Sie brauchen eine Strategie, die Ihnen Kontrolle, Transparenz und einen Rollback-Plan bietet.

    Semantische Versionierung

    Führen Sie semantische Versionierung (`MAJOR.MINOR.PATCH`) ein und machen Sie sie zum Bestandteil Ihres Workflows:

  • **MAJOR**: Inkompatible Änderungen, grundlegende Neugestaltungen
  • **MINOR**: Neue Funktionen, abwärtskompatibel
  • **PATCH**: Fehlerbehebungen, kleine Anpassungen
  • In Flutter befindet sich die Versionsinformation in der `pubspec.yaml`:

    yaml
    version: 2.4.1+42
    #        ^ Semver  ^ Build-Nummer

    Die Build-Nummer nach dem `+` muss mit jeder Store-Einreichung erhöht werden. In der CI automatisiere ich das über die Pipeline-Laufnummer:

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

    Automatisierte Changelogs

    Generieren Sie Changelogs aus Ihren Commit-Nachrichten. Das erfordert Disziplin bei Ihren Commit-Konventionen:

    bash
    # conventional-changelog installieren
    npm install -g conventional-changelog-cli
    
    # CHANGELOG.md generieren
    conventional-changelog -p angular -i CHANGELOG.md -s

    Wenn Ihr Team Conventional Commits (`feat:`, `fix:`, `chore:`) verwendet, wird dieser Prozess vollständig automatisch.

    Beta-Distribution

    Veröffentlichen Sie niemals direkt in die Produktion. Nutzen Sie einen gestuften Rollout:

  • **Interner Track** — Team und QA testen den Build.
  • **Geschlossene Beta** — Ausgewählte externe Nutzer geben Feedback.
  • **Offene Beta / Gestufter Rollout** — Schrittweise Verteilung an einen Prozentsatz der Nutzer.
  • **Volle Produktion** — Veröffentlichung für alle.
  • Für iOS dient TestFlight als Beta-Kanal. Für Android nutzen Sie die internen und geschlossenen Testkanäle von Google Play. Firebase App Distribution ist eine hervorragende Alternative für beide Plattformen, wenn Sie schnellere Feedback-Schleifen ohne Store-Review-Verzögerungen wünschen.

    Rollback-Plan

    Jedes Release sollte einen Notausgang haben. In der Praxis bedeutet das:

  • Halten Sie das Artefakt der vorherigen Version für ein erneutes Deployment bereit.
  • Verwenden Sie Feature Flags, um neue Funktionalität ohne neues Binary zu deaktivieren.
  • Auf Android können gestufte Rollouts angehalten und zurückgenommen werden. Auf iOS gibt es die Option "Aus dem Verkauf nehmen", aber ein einfacher Rollback ist nicht möglich — deshalb sind gründliche TestFlight-Tests entscheidend.
  • Häufige CI/CD-Fehler

    Diese Fehler habe ich immer wieder gesehen — in meinen eigenen Projekten und in Teams, mit denen ich zusammengearbeitet habe.

    1. Abhängigkeiten nicht cachen

    Flutter lädt Dart-Pakete, Gradle-Abhängigkeiten, CocoaPods und manchmal sogar das Flutter SDK selbst herunter. Ohne Caching verschwendet jeder Build 5-15 Minuten für Downloads.

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

    2. iOS-Builds auf Linux-Runnern ausführen

    iOS-Builds erfordern macOS. Das klingt offensichtlich, aber ich habe Pipelines gesehen, die `flutter build ipa` auf einem Ubuntu-Runner ausführen und dann stillschweigend scheitern. Verwenden Sie für iOS-Jobs immer `runs-on: macos-latest`.

    3. Secrets im Repository hartcodieren

    Keystores, API-Schlüssel, Service-Account-JSON-Dateien — nichts davon gehört in Ihr Git-Repository, nicht einmal in ein durch `.gitignore` ausgeschlossenes Verzeichnis. Nutzen Sie die verschlüsselte Secrets-Funktion Ihres CI-Anbieters oder einen dedizierten Secrets-Manager wie HashiCorp Vault.

    4. Tests überspringen, um die Pipeline zu "beschleunigen"

    Tests zu deaktivieren, weil sie "zu lange dauern", verfehlt den Zweck der CI. Wenn Ihre Tests langsam sind, optimieren Sie sie — verwenden Sie Mocks, führen Sie Tests parallel aus, lagern Sie Integrationstests in eine separate Stufe aus. Überspringen Sie sie nicht.

    5. Keine Build-Nummer-Strategie

    Wenn Ihre Build-Nummer nicht konsistent inkrementiert, werden Store-Uploads abgelehnt. Leiten Sie die Build-Nummer von etwas ab, das garantiert ansteigt: die CI-Laufnummer, ein Zeitstempel oder ein Zähler in Ihrer Versionskontrolle.

    6. Instabile Tests ignorieren

    Ein instabiler Test, der ignoriert wird, ist schlimmer als gar kein Test. Er gewöhnt das Team daran, CI-Fehler abzutun. Beheben oder entfernen Sie instabile Tests sofort.

    7. Build- und Deploy-Stufen nicht trennen

    Build und Deploy sollten getrennte Pipeline-Stufen sein. Wenn Ihr Deploy-Schritt fehlschlägt, sollten Sie dasselbe Artefakt ohne erneuten Build deployen können. Laden Sie Ihr Artefakt im Build-Schritt hoch und im Deploy-Schritt herunter.

    Alles zusammenführen

    Eine ausgereifte Flutter-CI/CD-Pipeline sieht so aus:

  • Der Entwickler pusht auf einen Feature-Branch.
  • CI führt bei jedem Push Formatprüfungen, statische Analyse und Tests aus.
  • Der PR wird in `develop` gemergt — CI baut und deployt auf den internen Testkanal.
  • Ein Release-Branch wird erstellt und getaggt (z.B. `v2.4.1`).
  • CI baut beide Plattformen, signiert Artefakte und generiert einen Changelog.
  • Die Artefakte werden auf TestFlight und den internen Google-Play-Track hochgeladen.
  • Nach QA-Freigabe befördert ein Promotion-Job den Build in die Produktion.
  • Die einzigen manuellen Schritte sind das Mergen des PRs, das Erstellen des Tags und die Freigabe der Promotion. Alles andere ist automatisiert.

    Fazit

    CI/CD ist für Flutter-Projekte kein Nice-to-have — es ist unverzichtbar. Die anfängliche Investition in die Pipeline-Konfiguration zahlt sich innerhalb der ersten Releases aus. Sie liefern schneller, verursachen weniger Fehler und schlafen ruhiger, weil Sie wissen, dass jedes Release denselben rigorosen, automatisierten Prozess durchlaufen hat.

    Ich helfe Ihnen gern bei der Entwicklung einer CI/CD-Pipeline, die auf Ihr Flutter-Projekt und Ihren Team-Workflow zugeschnitten ist.

    Verwandte Artikel

    Haben Sie ein Flutter-Projekt?

    Ich entwickle hochleistungsfähige Flutter-Anwendungen für iOS, Android und Web.

    Kontakt aufnehmen