Flutter
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:
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/*.ipaIn 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:
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: internalDas 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
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
endiOS Fastfile
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
endIn 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.
# Match zum ersten Mal initialisieren
fastlane match init
# Neue Zertifikate generieren
fastlane match appstore
fastlane match developmentWichtige 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 einekey.properties-Datei.
// 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:
version: 2.4.1+42
# ^ Semver ^ Build-NummerDie Build-Nummer nach dem + muss mit jeder Store-Einreichung erhöht werden. In der CI automatisiere ich das über die Pipeline-Laufnummer:
flutter build appbundle --build-number=$GITHUB_RUN_NUMBERAutomatisierte Changelogs
Generieren Sie Changelogs aus Ihren Commit-Nachrichten. Das erfordert Disziplin bei Ihren Commit-Konventionen:
# conventional-changelog installieren
npm install -g conventional-changelog-cli
# CHANGELOG.md generieren
conventional-changelog -p angular -i CHANGELOG.md -sWenn 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.
# 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
developgemergt — 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
Flutter Performance-Optimierung: Vollständiger Leitfaden
Steigern Sie die Performance Ihrer Flutter-App systematisch. Lernen Sie Rebuild-Optimierung, Speichermanagement, Lazy Loading und Profiling.
Flutter Testing: Unit-, Widget- und Integrationstests
Erstellen Sie eine praktikable Flutter-Teststrategie mit klaren Rollen für Unit-, Widget- und Integrationstests.
Flutter Firebase Integration: Auth, Firestore und Push
Richten Sie Firebase in Flutter ein: Auth-Flows, Firestore-Nutzung und grundlegende Sicherheit für Produktion.
Haben Sie ein Flutter-Projekt?
Ich entwickle hochleistungsfähige Flutter-Anwendungen für iOS, Android und Web.
Kontakt aufnehmen