Flutter CI/CD Workflow: A Simple Guide Using App Centre

- Do not start a new Flutter pipeline with App Center. Its build, test and distribution services are retired.
- Run formatting, static analysis and unit or widget tests on every pull request without loading production signing secrets.
- Build Android on Linux. Use a macOS runner for iOS because Xcode is required.
- Pin the Flutter SDK and commit
pubspec.lock for applications. Cloning the latest stable Flutter branch during every build is not reproducible.- Generate an Android App Bundle (
.aab) for Google Play and a signed .ipa for TestFlight or the App Store.Store keystores, certificates, API keys and passwords in protected CI secrets, not in Git or shell scripts.- Put production deployment jobs behind a GitHub environment with branch restrictions and required reviewers.
- Use fastlane, Google Play and TestFlight for store delivery. Firebase App Distribution is a practical option for pre-release testers.
- Replace old
flutter_driver tests with Flutter's integration_test package.- Treat build, distribution and monitoring as separate capabilities. App Center bundled them; a modern pipeline does not have to.
Mobile releases often fail because of process gaps rather than application code. A developer builds with one Flutter version, another machine uses different native tooling and someone uploads an artifact that was never tested in its final configuration.
CI/CD removes much of that uncertainty by making the same checks and build commands run every time.
The original version of this guide used Microsoft Visual Studio App Center. That workflow is no longer available. App Center's Build, Test and Distribution services were retired on March 31, 2025. Analytics and Diagnostics received a temporary extension, but that ended on June 30, 2026.
So this cannot honestly remain an App Center setup tutorial. Instead, I have rebuilt it as a 2026 migration guide using GitHub Actions for orchestration and fastlane for Android and iOS delivery. The same principles also apply if your team chooses Azure Pipelines, Codemagic or Bitrise.
What Is CI/CD for a Flutter App?
CI/CD is a set of automated checks and release steps that turns a source-code change into a tested, traceable application build.
Continuous Integration (CI) validates changes as developers open pull requests or merge code. A Flutter CI job normally installs the pinned SDK, resolves packages, checks formatting, runs flutter analyze, executes automated tests, and confirms that the platform projects compile.
Continuous Delivery (CD) prepares a signed build that is ready for a tester group or store. A controlled job creates the Android App Bundle or iOS IPA, applies signing credentials, assigns a unique build number, and uploads the artifact.
Continuous deployment goes one step further by releasing automatically after all gates pass. For mobile apps, I prefer automatic upload to an internal track or TestFlight, followed by a deliberate approval before production. Apple and Google review processes also mean that “deployment” is not always the same as immediate public availability.
A useful Flutter pipeline gives us:
- one repeatable SDK and dependency environment;
- fast feedback on pull requests;
- versioned Android and iOS artifacts;
- traceability from a store build to a Git commit;
- protected access to signing credentials; and
- fewer manual release steps.
If you want to go deeper into quality gates beyond this Flutter example, see our guide to implementing quality assurance in a CI/CD pipeline.
Why the Old App Center Workflow No Longer Works
The previous article instructed readers to create Android and iOS apps in the App Center dashboard, connect a repository, upload signing files and add appcenter-post-clone.sh scripts.
Those steps should be removed, not lightly edited. Microsoft says App Center retired on March 31, 2025 and that users would no longer be able to sign in or call its APIs after retirement. Its temporary Analytics and Diagnostics extension ended in June 2026.
The old scripts also had technical weaknesses independent of the shutdown:
- they cloned Flutter's moving
stablebranch during each build; - they used
set -x, which can expose sensitive command data in logs; - they ran
flutter cleanon every build, defeating useful caches; - the Android script built an APK even though Google Play prefers App Bundles;
- they referred to the retired
flutter_driverapproach; and - signing and distribution logic lived in a vendor-specific dashboard rather than reviewed pipeline code.
Microsoft's retirement guidance recommends Azure Pipelines for App Center Build migrations, BrowserStack App Automate for device testing, TestFlight and Google Play for distribution, and other Azure-connected services for analytics and diagnostics.
Those are valid migration targets. This guide uses GitHub Actions because its workflow can live beside the Flutter source code and is easy to review. If your organization is already standardized on Azure DevOps, the same commands can run in Azure Pipelines.
Mapping App Center Features to a 2026 Toolchain
| App Center capability | Practical replacement | Responsibility |
| Build | GitHub Actions, Azure Pipelines, Codemagic or Bitrise | Install toolchains and compile Android or iOS |
| Test | flutter test, integration_test, emulator jobs and a device cloud | Validate Dart, widgets, integrations and physical devices |
| Distribution | Google Play internal testing, TestFlight or Firebase App Distribution | Deliver signed builds to testers |
| Production release | Google Play and App Store Connect, commonly automated with fastlane | Upload store artifacts and metadata |
| Analytics | A supported product such as Firebase Analytics, Azure-connected monitoring or another analytics platform | Measure product usage |
| Crash diagnostics | Firebase Crashlytics, Sentry, Azure-connected monitoring or another diagnostics platform | Capture and investigate failures |
Do not choose one product merely because it replaces the largest number of App Center menu items. Build orchestration, device testing, tester distribution and production monitoring have different security and operational requirements.
The Workflow We Will Build
The pipeline has two boundaries:
- CI on pull requests and
main: format, analyze, test and perform unsigned or development builds. This workflow does not receive production credentials. - CD on a version tag or manual release: obtain approval, restore signing credentials, create store artifacts and upload them to controlled release tracks.
This separation matters. Forked pull requests and unreviewed branches should never be able to read production signing secrets.
Prerequisites
Before adding the workflows, confirm that:
- the Flutter project builds locally for Android and iOS;
pubspec.lockis committed for the application;- Android release signing is configured and the upload keystore is backed up;
- the app already exists in Google Play Console and App Store Connect;
- the team has an Apple Developer Program membership;
- fastlane is initialized and tested locally before it runs in CI; and
- your repository can use GitHub Actions and protected environments.
Xcode is required for iOS builds, so iOS jobs must run on macOS. Android CI can run on Linux, which is usually faster and less expensive than a macOS runner.
1. Pin the Flutter Version
The first App Center script cloned whatever happened to be on Flutter's stable branch. That makes a build change even when the application code has not changed.
Pin one tested Flutter SDK version. The current Flutter documentation reflects Flutter 3.44.7 as of July 31, 2026, so the example below uses it. Replace it deliberately when your project upgrades.
Add an exact Flutter constraint to pubspec.yaml:
environment:
sdk: ">=3.0.0 <4.0.0"
flutter: "3.44.7"The GitHub workflow can then read the version from this file. Teams using FVM can point the setup action at .fvmrc instead.
Also commit the lockfile:
git add pubspec.yaml pubspec.lock
git commit -m "Pin Flutter and Dart dependencies"Applications should normally commit pubspec.lock so CI resolves the same package versions developers tested. Reusable Dart or Flutter packages follow different lockfile conventions because their compatibility range must be tested more broadly.
Keep the release version and monotonically increasing build number in the same file:
version: 1.4.0+140Flutter maps the value after + to Android's versionCode and Apple's build version. Increment it beyond the last build already uploaded through App Center or another pipeline before creating the release tag. This is safer during a migration than assuming a new CI provider's run counter starts above the existing store build number.
Let’s Build Your Flutter App Together!
Work with our expert team to turn your app idea into a fast, stunning Flutter product.
2. Add a Pull-Request CI Workflow
Create .github/workflows/flutter-ci.yml:
name: Flutter CI
on:
pull_request:
push:
branches: [main]
permissions:
contents: read
concurrency:
group: flutter-ci-${{ github.ref }}
cancel-in-progress: true
jobs:
android:
name: Analyze, test and build Android
runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@v6
- name: Set up Java
uses: actions/setup-java@v5
with:
distribution: temurin
java-version: "17"
cache: gradle
- name: Set up Flutter
uses: subosito/flutter-action@v2
with:
channel: stable
flutter-version-file: pubspec.yaml
cache: true
- name: Install packages
run: flutter pub get
- name: Check formatting
run: dart format --output=none --set-exit-if-changed .
- name: Analyze source
run: flutter analyze --fatal-infos
- name: Run tests
run: flutter test --coverage
- name: Build development APK
run: flutter build apk --debug
- name: Upload Android artifact
uses: actions/upload-artifact@v5
with:
name: android-debug-${{ github.sha }}
path: build/app/outputs/flutter-apk/app-debug.apk
if-no-files-found: error
retention-days: 7
ios:
name: Compile iOS without signing
runs-on: macos-15
steps:
- name: Check out repository
uses: actions/checkout@v6
- name: Set up Flutter
uses: subosito/flutter-action@v2
with:
channel: stable
flutter-version-file: pubspec.yaml
cache: true
- name: Install packages
run: flutter pub get
- name: Compile iOS release
run: flutter build ios --release --no-codesignThis workflow gives every pull request four useful gates:
- formatting must be stable;
- static analysis must pass;
- automated tests must pass; and
- both platform projects must compile.
The Android artifact is a development APK for review, not a Play Store release. The iOS job intentionally compiles without signing. Production credentials remain outside the pull-request workflow.
macos-15 is pinned here instead of macos-latest to reduce surprise Xcode changes. Runner images still receive updates, so record the Xcode and Flutter versions in build logs and test upgrades deliberately.
3. Add Integration Tests Without Flutter Driver
The original article suggested Flutter Driver. Flutter now directs projects to the integration_test package.
Add it under dev_dependencies:
dev_dependencies:
flutter_test:
sdk: flutter
integration_test:
sdk: flutterPlace tests in integration_test/ and run them against an emulator, simulator or connected device:
flutter test integration_testDo not add a device-dependent command to the basic Linux job and assume it will work. Create a separate job that boots the required emulator or sends the build to a device-testing provider. Keep unit and widget tests fast so developers receive early feedback, then run the slower integration suite after those gates pass.
4. Configure Android Release Signing Securely
Google Play prefers an Android App Bundle. Flutter creates it with:
flutter build appbundle --releaseThe output is:
build/app/outputs/bundle/release/app-release.aabDo not commit the upload keystore or android/key.properties. Keep an encrypted backup outside CI and store the following values as protected environment secrets:
ANDROID_KEYSTORE_BASE64ANDROID_STORE_PASSWORDANDROID_KEY_ALIASANDROID_KEY_PASSWORDPLAY_SERVICE_ACCOUNT_JSON_BASE64
Base64 is transport encoding, not encryption. The security comes from the CI secret store and access policy.
A release job can reconstruct the files only for the duration of the runner:
- name: Restore Android signing files
shell: bash
env:
KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
STORE_PASSWORD: ${{ secrets.ANDROID_STORE_PASSWORD }}
KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
run: |
echo "$KEYSTORE_BASE64" | base64 --decode > android/app/upload-keystore.jks
{
echo "storePassword=$STORE_PASSWORD"
echo "keyPassword=$KEY_PASSWORD"
echo "keyAlias=$KEY_ALIAS"
echo "storeFile=upload-keystore.jks"
} > android/key.properties
- name: Build signed Android App Bundle
run: flutter build appbundle --releaseNever add set -x to a step that handles secrets. GitHub masks registered secret values, but avoiding unnecessary command tracing is a safer design.
Use Play App Signing and protect the upload key. Losing an upload key is recoverable through Google's reset process when Play App Signing is enabled, but an undocumented or unowned signing setup still causes release delays.
5. Upload Android Builds With fastlane
Flutter's official continuous-delivery guide recommends testing fastlane locally before moving it to a cloud runner.
Add fastlane to a committed Gemfile:
source "https://rubygems.org"
gem "fastlane"Create android/fastlane/Fastfile:
default_platform(:android)
platform :android do
desc "Upload the Flutter bundle to Google Play internal testing"
lane :internal do
upload_to_play_store(
track: "internal",
aab: "../build/app/outputs/bundle/release/app-release.aab",
json_key: "play-store-key.json",
skip_upload_metadata: true,
skip_upload_images: true,
skip_upload_screenshots: true
)
end
endIn the release workflow, decode the Google Play service-account key and run the lane:
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
bundler-cache: true
- name: Restore Google Play credentials
env:
PLAY_KEY_BASE64: ${{ secrets.PLAY_SERVICE_ACCOUNT_JSON_BASE64 }}
run: echo "$PLAY_KEY_BASE64" | base64 --decode > android/play-store-key.json
- name: Upload to Google Play internal testing
run: cd android && bundle exec fastlane internalUse the internal testing track first. Promotion from internal to production should be a separate, reviewable action rather than an automatic side effect of compiling the app.
6. Build and Upload iOS Through TestFlight
iOS delivery has two independent security requirements:
- Code signing: a distribution certificate and provisioning profile, or a managed signing workflow such as fastlane match.
- App Store Connect authentication: preferably an App Store Connect API key for CI uploads.
The API key can authorize an upload, but it does not sign the application. Configure and test signing locally before automating it.
Once signing is available on the macOS runner, Flutter can create the IPA:
flutter build ipa --releaseThe generated file is placed under build/ios/ipa/.
An iOS fastlane lane can upload that artifact to TestFlight:
default_platform(:ios)
platform :ios do
desc "Upload the signed Flutter IPA to TestFlight"
lane :beta do
api_key = app_store_connect_api_key(
key_id: ENV.fetch("ASC_KEY_ID"),
issuer_id: ENV.fetch("ASC_ISSUER_ID"),
key_content: ENV.fetch("ASC_KEY_BASE64"),
is_key_content_base64: true
)
ipa = Dir["../build/ios/ipa/*.ipa"].first
UI.user_error!("No IPA found") unless ipa
upload_to_testflight(
api_key: api_key,
ipa: ipa,
skip_waiting_for_build_processing: true
)
end
endRun that lane from the ios directory after building:
- name: Upload to TestFlight
env:
ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }}
ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}
ASC_KEY_BASE64: ${{ secrets.ASC_KEY_BASE64 }}
run: cd ios && bundle exec fastlane betaStore signing material and App Store Connect credentials in a protected production environment. Do not expose them to pull-request jobs.
7. Protect the Release Workflow
Triggering production delivery on every push is fast, but it is rarely the safest first implementation.
A controlled release workflow can run on a semantic version tag and also support a manual trigger:
name: Flutter Release
on:
push:
tags:
- "v*"
workflow_dispatch:
permissions:
contents: read
jobs:
release:
environment: production
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
# Restore tools and signing files, build, then upload.Configure the production environment with:
- required reviewers;
- permitted deployment branches or tags;
- environment-level secrets;
- a rule preventing self-approval where appropriate; and
- a concurrency policy that prevents overlapping releases.
Let’s Build Your Flutter App Together!
Work with our expert team to turn your app idea into a fast, stunning Flutter product.
GitHub does not expose environment secrets to the job until its protection rules pass. That makes the environment a meaningful security boundary rather than a label.
For stronger supply-chain controls, pin third-party actions to reviewed commit SHAs and use an update process such as Dependabot to propose changes. Major-version tags make examples readable, but an immutable SHA gives production workflows a stronger guarantee about the code being executed.
8. Replace App Center Distribution for Testers
App Center Distribution used to provide one tester portal for both platforms. In 2026, the simplest supported choices are:
- Google Play internal testing for Android builds close to production;
- TestFlight internal or external testing for iOS;
- Firebase App Distribution for cross-platform pre-release groups; or
- an enterprise mobile-device-management platform for internal corporate apps.
Firebase App Distribution can accept builds through its CLI, fastlane or Gradle. For example, after producing a signed Android APK:
firebase appdistribution:distribute \
build/app/outputs/flutter-apk/app-release.apk \
--app "$FIREBASE_ANDROID_APP_ID" \
--groups "qa-team" \
--release-notes "Commit $GITHUB_SHA"This is a distribution step, not a replacement for CI. The app must still be built, signed and tested before it is uploaded.
9. Notifications and Observability
A failed build should notify the people who can act on it. GitHub already exposes pull-request checks and can send platform notifications. Slack or Microsoft Teams notifications can be added through approved integrations or incoming webhooks.
Avoid sending an alert for every successful step. Useful notifications normally include:
- failed
mainbuilds; - failed production releases;
- releases waiting for approval;
- successful uploads to a tester track; and
- production crash or regression alerts from the monitoring platform.
CI logs are not application monitoring. Replace App Center Analytics and Diagnostics separately, confirm that the new SDK respects consent and privacy requirements, and verify that symbols or mapping files are uploaded so production crashes are readable.
App Center Migration Checklist
If the project previously used App Center, work through this migration deliberately:
- Inventory every App Center capability the app depended on: build, test, distribution, analytics, crashes, push or CodePush.
- Recover signing files, tester lists, build variables and historical configuration from organizational backups or retained documentation.
- Rotate credentials that were stored in App Center and add the replacements to the new secret store.
- Remove
appcenter-post-clone.shand other dead build hooks after the new pipeline is proven. - Remove or replace retired App Center SDK integrations in the application.
- Migrate Flutter Driver tests to
integration_test. - Reproduce Android and iOS release builds locally with the pinned Flutter version.
- Compare application IDs, entitlements, signing identities, version numbers and artifact formats with the last known release.
- Upload first to an internal Play track and TestFlight group.
- Document ownership, renewal and recovery for every certificate, key and service account.
Do not delete the old release documentation until the new pipeline has produced installable, signed builds on both platforms.
Common Flutter CI/CD Mistakes
| Mistake | Why it causes problems | Better approach |
| Cloning Flutter's latest stable branch during each build | The toolchain changes without a source commit | Pin an exact tested Flutter version |
| Running release jobs on pull requests | Untrusted code may reach signing secrets | Keep CI secret-free and release only from protected refs |
Committing a keystore or .p12 file | Repository access becomes signing access | Use encrypted CI secrets and an external recovery backup |
| Building only an APK for Google Play | APKs miss App Bundle delivery benefits | Build a signed .aab for Play releases |
Using flutter_driver in a new pipeline | The project has moved to integration_test | Migrate the tests before automating them |
| Using one build number repeatedly | Stores reject or confuse duplicate uploads | Generate a monotonically increasing build number |
| Uploading directly to production | A successful compile is not release approval | Use internal tracks, TestFlight and protected promotion |
| Treating analytics as part of the build tool | It couples unrelated lifecycle decisions | Select and migrate monitoring separately |
Choosing a Flutter CI/CD Platform
GitHub Actions is not the only valid App Center replacement.
| Platform | Best fit | Trade-off |
| GitHub Actions | Teams already hosting code on GitHub and wanting pipeline-as-code | iOS signing and store delivery require deliberate setup |
| Azure Pipelines | Organizations standardized on Azure DevOps or following Microsoft's migration path | Flutter-specific conveniences may require more scripting |
| Codemagic | Flutter teams wanting managed mobile workflows and signing assistance | Adds a specialist CI vendor and pricing model |
| Bitrise | Mobile teams wanting visual workflows and broad mobile integrations | Complex workflows can become platform-specific |
| Xcode Cloud | Apple-focused teams needing native Xcode and App Store integration | It does not replace the Android pipeline |
Choose based on runner availability, signing support, secret controls, auditability, build minutes and the team's ability to maintain the pipeline. The Flutter commands should remain portable even if the orchestration provider changes.
Conclusion
App Center once made Flutter build and tester distribution approachable, but it is no longer a usable foundation in 2026. Preserving its dashboard instructions would create a polished guide to a dead end.
A current Flutter CI/CD workflow should keep the important ideas while removing the vendor dependency: pin the Flutter SDK, validate every pull request, compile both platform projects, isolate production secrets, create the correct store artifacts and release through protected environments.
GitHub Actions and fastlane are one practical combination. Azure Pipelines, Codemagic and Bitrise can implement the same lifecycle. The durable part is not the logo on the CI dashboard; it is the reviewed, reproducible path from a commit to a signed release.
Frequently Asked Questions
Can I still use Microsoft App Center for Flutter CI/CD?
No. App Center's build, test and distribution capabilities retired in March 2025. Its extended Analytics and Diagnostics support ended in June 2026, so new Flutter pipelines need supported replacements today.
What is the best App Center replacement for Flutter?
There is no universal replacement for teams. GitHub Actions offers flexible pipeline-as-code, Azure Pipelines follows Microsoft's migration path, while Codemagic and Bitrise provide more specialized mobile and Flutter workflow features.
Do Flutter iOS builds require a macOS CI runner?
Yes. Flutter iOS release builds require Xcode, which runs on macOS. Android jobs can run on Linux, but signed IPA generation and validation must use a compatible macOS environment today.
Where should Flutter signing credentials be stored?
Store keystores, certificates, provisioning profiles, passwords and API keys in protected CI environment secrets. Keep encrypted recovery backups separately, restrict release access and never commit credentials to Git under any circumstances.
Should every merge automatically publish the Flutter app?
Usually not at first. Automate testing and internal-track uploads, then require approval before production promotion by default. This preserves delivery speed while protecting signing credentials and preventing accidental public releases.



