← Articles

Automating version and build number bumping in Flutter

By Ann Tech · 2 November 2024

Version and build numbers are easy to forget, easy to get wrong, and rejections from the App Store or Play Store because of a version conflict are annoying. Automating them in CI means you never have to think about them.

How Flutter versions work

In pubspec.yaml:

version: 2.5.1+47
#        ^^^^^  version name shown to users (app store listing)
#               ^^ build number (must be unique per submission)
  • versionName (2.5.1): shown in About screens and store listings
  • versionCode/buildNumber (47): must be strictly increasing for each upload

Strategy 1: CI build number

Use the CI run number as the build number — it's already unique and incrementing:

# GitHub Actions
flutter build apk \
  --build-name=${{ github.ref_name }} \
  --build-number=${{ github.run_number }}

This means build 1, 2, 3... regardless of version name. Simple and guaranteed unique.

Strategy 2: git commit count

BUILD_NUMBER=$(git rev-list --count HEAD)
flutter build apk \
  --build-name=2.5.1 \
  --build-number=$BUILD_NUMBER

Git commit count increases monotonically with every commit, which means every CI build from main gets a unique build number.

Strategy 3: timestamp

BUILD_NUMBER=$(date +%Y%m%d%H%M)
flutter build apk --build-number=$BUILD_NUMBER

Use with caution — timestamps can collide if two builds start in the same minute.

Fastlane increment_build_number

Fastlane can read the current version from App Store Connect and increment:

lane :bump do
  # Get the latest build number from TestFlight and increment
  latest = latest_testflight_build_number(
    api_key_path: 'fastlane/app_store_connect_api_key.json',
  )
  increment_build_number(
    build_number: latest + 1,
    xcodeproj: 'ios/Runner.xcodeproj',
  )
  # Also update Android
  android_set_version_code(
    gradle_file: 'android/app/build.gradle.kts',
    version_code: latest + 1,
  )
end

Automating version name bumps

For semantic version names (2.5.1 → 2.5.2), use a script:

#!/bin/bash
# scripts/bump_version.sh

CURRENT=$(grep '^version: ' pubspec.yaml | sed 's/version: //' | cut -d'+' -f1)
BUILD=$(grep '^version: ' pubspec.yaml | sed 's/.*+//')

# Parse semver
MAJOR=$(echo $CURRENT | cut -d. -f1)
MINOR=$(echo $CURRENT | cut -d. -f2)
PATCH=$(echo $CURRENT | cut -d. -f3)

case $1 in
  major) MAJOR=$((MAJOR + 1)); MINOR=0; PATCH=0 ;;
  minor) MINOR=$((MINOR + 1)); PATCH=0 ;;
  patch) PATCH=$((PATCH + 1)) ;;
  *) echo "Usage: $0 [major|minor|patch]"; exit 1 ;;
esac

NEW_VERSION="$MAJOR.$MINOR.$PATCH"
NEW_BUILD=$((BUILD + 1))

# Update pubspec.yaml
sed -i '' "s/^version: .*/version: $NEW_VERSION+$NEW_BUILD/" pubspec.yaml

echo "Bumped to $NEW_VERSION+$NEW_BUILD"

Usage:

bash scripts/bump_version.sh patch  # 2.5.1 → 2.5.2
bash scripts/bump_version.sh minor  # 2.5.1 → 2.6.0
bash scripts/bump_version.sh major  # 2.5.1 → 3.0.0

Reading the version in the app

dependencies:
  package_info_plus: ^8.0.0
final info = await PackageInfo.fromPlatform();

Text('Version ${info.version} (${info.buildNumber})')
// "Version 2.5.1 (47)"

CI workflow example

name: Release

on:
  workflow_dispatch:
    inputs:
      bump_type:
        type: choice
        options: [patch, minor, major]
        default: patch

jobs:
  release:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          token: ${{ secrets.GH_PAT }}  # Needed for push

      - name: Bump version
        run: bash scripts/bump_version.sh ${{ inputs.bump_type }}

      - name: Get new version
        id: version
        run: |
          VERSION=$(grep '^version: ' pubspec.yaml | sed 's/version: //' | cut -d'+' -f1)
          echo "version=$VERSION" >> $GITHUB_OUTPUT

      - name: Build
        run: flutter build appbundle --build-number=${{ github.run_number }}

      - name: Commit and tag
        run: |
          git config user.email '[email protected]'
          git config user.name 'CI'
          git commit -am 'chore: bump version to ${{ steps.version.outputs.version }}'
          git tag v${{ steps.version.outputs.version }}
          git push origin main --tags

Common pitfalls

Using the same build number twice. App Store Connect and Play Console reject duplicate build numbers. If CI retries a failed build, the second attempt needs a different build number. Using run_number rather than a version-based counter prevents this.

Forgetting to sync Android and iOS build numbers. Both platforms need unique incrementing numbers. If you update one but not the other, the release metadata gets out of sync. Automate both together.

Committing pubspec.yaml changes from CI to main. If CI pushes a version bump commit, other PRs need to rebase onto it. Use a dedicated release branch or tag rather than committing version bumps to main.

Sign in to like, dislike, or report.

Automating version and build number bumping in Flutter — ANN Tech