Flutter

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

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:

  1. Code-Qualitätsprüfungen — Lint, Format, statische Analyse
  2. Automatisierte Tests — Unit-, Widget- und Integrationstests
  3. Build und Artefakt-Erzeugung — plattformspezifische Binaries
  4. 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:

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

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:

  1. Interner Track — Team und QA testen den Build.
  2. Geschlossene Beta — Ausgewählte externe Nutzer geben Feedback.
  3. Offene Beta / Gestufter Rollout — Schrittweise Verteilung an einen Prozentsatz der Nutzer.
  4. 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:

  1. Der Entwickler pusht auf einen Feature-Branch.
  2. CI führt bei jedem Push Formatprüfungen, statische Analyse und Tests aus.
  3. Der PR wird in develop gemergt — CI baut und deployt auf den internen Testkanal.
  4. Ein Release-Branch wird erstellt und getaggt (z.B. v2.4.1).
  5. CI baut beide Plattformen, signiert Artefakte und generiert einen Changelog.
  6. Die Artefakte werden auf TestFlight und den internen Google-Play-Track hochgeladen.
  7. 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