Dart Tutorial

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
PartNameRule
2.4.1Version nameWhat users see. Follow semantic versioning
57Build numberMust 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 match tool, 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 #

ServiceNotes
GitHub ActionsGeneral purpose. Free minutes for public and small private projects
CodemagicBuilt for Flutter. Handles iOS signing and store uploads with little setup
Bitrise, CircleCI, GitLab CIGeneral 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 #

  1. Work on a feature branch.
  2. Open a pull request. CI formats, analyses and tests it.
  3. Merge to main when it is green and reviewed.
  4. Main builds automatically to internal testing.
  5. 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.

WatchWith
CrashesFirebase Crashlytics, Sentry
PerformanceFirebase Performance, the stores’ vitals pages
UsageFirebase Analytics or a privacy-friendly alternative
ReviewsThe 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.

  1. First, the CI file above: format, analyse, test on every pull request.
  2. Then automate the Android build.
  3. Then delivery to internal testers.
  4. 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.

Practise in the playground Updated by Santosh Adhikari