Back to Writing

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.