2026-04-12 · 6 min read
Shipping a Flutter app to two stores without losing your weekend
The release pipeline I wish someone had handed me on day one , signing, version bumps, and the small scripts that make it boring.
The first time I shipped a Flutter app to both stores, it took two full days. The second time, twenty minutes. The difference wasn’t tooling , it was writing down the boring steps so I wouldn’t forget them under pressure.
Bumping the version in one place
Both Android and iOS read from pubspec.yaml, so that’s where the version
lives. The format is major.minor.patch+build:
name: tasklight
description: A minimal task app
publish_to: 'none'
version: 1.4.2+42
environment:
sdk: '>=3.0.0 <4.0.0'
I wrote a tiny script that bumps the build number on every release. It reads
the current version, increments the part after +, and writes it back:
#!/usr/bin/env bash
set -euo pipefail
current=$(grep '^version:' pubspec.yaml | awk '{print $2}')
name="${current%+*}"
build="${current#*+}"
next=$((build + 1))
sed -i.bak "s/^version: .*/version: ${name}+${next}/" pubspec.yaml
rm pubspec.yaml.bak
echo "Bumped to ${name}+${next}"
Signing without storing keys in the repo
For Android, the keystore lives outside the repo and the path is read from a local file that’s gitignored:
storePassword=...
keyPassword=...
keyAlias=upload
storeFile=/Users/me/keys/tasklight-upload.jks
Then in app/build.gradle, the signing config reads from that file at build
time:
def keystoreProperties = new Properties()
def keystorePropertiesFile = rootProject.file('key.properties')
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
}
android {
signingConfigs {
release {
keyAlias keystoreProperties['keyAlias']
keyPassword keystoreProperties['keyPassword']
storeFile file(keystoreProperties['storeFile'])
storePassword keystoreProperties['storePassword']
}
}
}
One command to ship
The whole release flow is wrapped in a Makefile so I never have to remember the flags:
.PHONY: release-android release-ios
release-android:
./scripts/bump_version.sh
flutter build appbundle --release
@echo "Bundle at build/app/outputs/bundle/release/app-release.aab"
release-ios:
flutter build ipa --release --export-options-plist=ios/ExportOptions.plist
@echo "IPA at build/ios/ipa/"
What I’d do differently
Next time, I’d add Fastlane on top of this on day one instead of waiting until release #4. The shell scripts work fine for one person, but the moment a second developer touched the project, everyone needed an opinion on how to bump versions. Fastlane would have made that conversation shorter.