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 listingsversionCode/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.