Flutter Lesson 80 of 83 5 min read
CI/CD and Versioning for Flutter Apps
Automate Flutter analysis, tests and builds with GitHub Actions, manage version numbers, and deliver releases with Fastlane or Codemagic.
On this page
Building and uploading by hand works for a first release. It becomes error-prone by the tenth: a forgotten test run, a build from the wrong branch, a version number not raised. Continuous integration (CI) runs your checks automatically on every change. Continuous delivery (CD) builds and ships the app automatically.
What a pipeline does #
push or pull request
│
▼
flutter pub get
dart format --check
flutter analyze
flutter test
│ (all must pass)
▼
build: Android bundle, iOS archive, web
│
▼
deliver: internal testing, TestFlight, hosting
Versioning #
In pubspec.yaml:
version: 2.4.1+57
| Part | Name | Rule |
|---|---|---|
2.4.1 | Version name | What users see. Follow semantic versioning |
57 | Build number | Must rise with every upload to a store. Users do not see it |
Semantic versioning for apps:
- Major (
2): a redesign or a big change in what the app does. - Minor (
4): new features. - Patch (
1): bug fixes.
You can override both at build time, which is how CI sets them without editing the file:
flutter build appbundle --build-name=2.4.1 --build-number=57
A common scheme is to use the CI run number as the build number, so it rises automatically.
Mark each release in Git:
git tag v2.4.1
git push origin v2.4.1
CI with GitHub Actions #
Create .github/workflows/ci.yml:
name: CI
on:
pull_request:
push:
branches: [main]
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: subosito/flutter-action@v2
with:
channel: stable
cache: true
- run: flutter pub get
- run: dart format --output=none --set-exit-if-changed .
- run: flutter analyze
- run: flutter test
Every pull request now shows a green tick or a red cross. Protect the main branch so that nothing merges while the checks fail. This one file prevents most “it worked on my machine” problems.
Building Android in CI #
Secrets such as the keystore must never be in the repository. Store them in the repository’s Secrets settings, and write them to files during the run.
build-android:
needs: check
if: startsWith(github.ref, 'refs/tags/v')
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:
channel: stable
cache: true
- name: Restore the keystore
run: |
echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 --decode > android/upload-keystore.jks
cat > android/key.properties <<EOF
storePassword=${{ secrets.KEYSTORE_PASSWORD }}
keyPassword=${{ secrets.KEY_PASSWORD }}
keyAlias=upload
storeFile=../upload-keystore.jks
EOF
- run: flutter build appbundle --build-number=${{ github.run_number }}
- uses: actions/upload-artifact@v4
with:
name: app-bundle
path: build/app/outputs/bundle/release/app-release.aab
This job runs only when you push a tag such as v2.4.1. To produce the secret, encode the keystore on your machine with base64 and paste the text into GitHub.
The Java version and action versions change over time. Use the ones the tools currently recommend.
Building iOS in CI #
iOS needs a macOS runner and signing certificates, which makes it the hardest part to automate. Two practical routes:
- Fastlane with its
matchtool, which stores certificates in a private repository and installs them during the build. - Codemagic or another Flutter-focused service, which manages iOS signing through a web interface.
Delivering automatically #
Fastlane is the standard tool for uploading to the stores.
# android/fastlane/Fastfile
lane :internal do
upload_to_play_store(
track: 'internal',
aab: '../build/app/outputs/bundle/release/app-release.aab'
)
end
# ios/fastlane/Fastfile
lane :beta do
upload_to_testflight(ipa: '../build/ios/ipa/Runner.ipa')
end
Each needs credentials: a service account for Google Play, and an App Store Connect API key for Apple.
Firebase App Distribution sends test builds to testers on both platforms without going through the stores.
Hosted CI services #
| Service | Notes |
|---|---|
| GitHub Actions | General purpose. Free minutes for public and small private projects |
| Codemagic | Built for Flutter. Handles iOS signing and store uploads with little setup |
| Bitrise, CircleCI, GitLab CI | General purpose, with Flutter support |
For a solo developer, a Flutter-focused service saves a lot of setup time. For a team already on GitHub, Actions plus Fastlane is a common choice.
A branching flow that works with CI #
- Work on a feature branch.
- Open a pull request. CI formats, analyses and tests it.
- Merge to main when it is green and reviewed.
- Main builds automatically to internal testing.
- When a build is good, tag it. The tag triggers the production build and upload.
Environments #
Build development and production variants from the same code with --dart-define or flavors, as described under dependency injection and config. CI supplies the production values from its secrets.
Updates without the store #
Store review takes days. Code push services such as Shorebird can deliver changes to an app’s Dart code directly to users’ devices, within the stores’ rules. They are useful for urgent fixes. Native code and assets still need a normal release.
Remote Config (Firebase and others) lets you switch features on and off, or change values, without any release at all. Shipping a feature turned off and enabling it later is called a feature flag.
After release #
Shipping is the start of the feedback loop.
| Watch | With |
|---|---|
| Crashes | Firebase Crashlytics, Sentry |
| Performance | Firebase Performance, the stores’ vitals pages |
| Usage | Firebase Analytics or a privacy-friendly alternative |
| Reviews | The store consoles. Reply to them |
Use a staged rollout for each update, and stop it if the crash rate rises.
Start small #
You do not need all of this on day one.
- First, the CI file above: format, analyse, test on every pull request.
- Then automate the Android build.
- Then delivery to internal testers.
- Then iOS.
Each step removes a manual task that somebody would eventually get wrong.
Try it yourself #
Push one of your Flutter projects to GitHub and add the CI workflow from this lesson. Open a pull request that breaks a test and watch it fail, then fix it and watch it pass. Add a step that builds a debug APK and uploads it as an artifact.