How to cut and ship a new UNIUN release for iOS and Android. Follow the steps in
order. Everything user-facing is driven from one place — the version line in
pubspec.yaml.
- Bump the version in
pubspec.yaml. - Update the changelog in
CHANGELOG.md. - Make sure signing is set up (keystore +
key.propertiesfor Android, certs for iOS). - Build the release artifact (
.ipafor iOS,.aabfor Android). - Upload & tag.
The version lives on a single line:
version: 1.1.0+2It has two parts:
1.1.0 +2
└─ name ─┘ └ build ┘
-
Version name (
1.1.0) — the human-facingMAJOR.MINOR.PATCHshown in stores. Bump it according to Semantic Versioning:Change type Example What moves Bug fix (patch) 1.2.0+15 → 1.2.1+16PATCH New feature (backwards-compatible) 1.2.0+15 → 1.3.0+16MINOR (PATCH resets to 0) Major redesign / breaking change 1.2.0+15 → 2.0.0+16MAJOR (MINOR + PATCH reset to 0) -
Build number (
+2) — the integer after the+. This becomes AndroidversionCodeand the iOS build number (android/app/build.gradle.ktsreadsflutter.versionCode/flutter.versionNamefrom this line). It must strictly increase on every single upload to a store, even for a re-upload of the same version name. Both Google Play and App Store Connect reject a build whose number was already used. When in doubt, bump it.
⚠️ A version with no+buildsuffix (e.g. just1.1.0) makes Flutter default the build number to1. Always include an explicit, increasing build number before a store upload.
Add a new section at the top of CHANGELOG.md, above the previous release.
Follow the existing format (Keep a Changelog): group entries under Added / Changed / Fixed.
## [1.2.0] — 2026-07-01
### Added
- Short, user-facing description of the new feature.
### Changed
- What behaviour or UI changed.
### Fixed
- The bug, in one line.1.0.0 was the initial release; every release since is appended here. Keep entries
user-facing and concise — this is the same text that feeds store "What's New" notes.
A release build must be signed. Debug builds are signed automatically; release builds are not. Set this up once per machine and keep the secrets safe.
The release signingConfig in android/app/build.gradle.kts reads four values from a file at
android/key.properties (kept out of git — see android/.gitignore, which ignores
key.properties, **/*.jks, and **/*.keystore).
a) Generate the keystore (once, ever). Pick a location outside the repo:
keytool -genkey -v \
-keystore ~/uniun-upload-keystore.jks \
-keyalg RSA -keysize 2048 -validity 10000 \
-alias uploadYou'll be prompted for a store password, a key password, and your name/org. Remember these.
🔐 Back this file up forever. If you lose the keystore (or its passwords) you can no longer publish updates to the same Google Play listing — you'd have to ship a brand-new app. Store it in a password manager / secure vault, not on a single laptop. Never commit it.
b) Create android/key.properties (this file is gitignored — never commit it):
storePassword=<the store password you chose>
keyPassword=<the key password you chose>
keyAlias=upload
storeFile=/Users/you/uniun-upload-keystore.jksstoreFile is resolved relative to the android/ folder, so an absolute path is safest. If you
place the keystore inside android/, a relative path like storeFile=upload-keystore.jks also
works (it's gitignored).
If key.properties is missing, the build falls back to no release signing and the resulting
.aab will not be accepted by the Play Store.
iOS signing is managed by Xcode / Apple, not by a file in the repo. You need:
- An Apple Developer Program membership (the team that owns the
in.uniun.appbundle ID). - A Distribution certificate and an App Store provisioning profile for the bundle ID.
- The signing Team selected in Xcode: open
ios/Runner.xcworkspace→ Runner target → Signing & Capabilities. "Automatically manage signing" is the simplest path; Xcode then fetches/creates the cert + profile for you. - An App Store Connect record for the app (created once, under the same bundle ID) before the first upload.
Building an .ipa for the store requires running on macOS with Xcode installed.
Clean first if you changed dependencies or native config:
flutter clean && flutter pub get
flutter pub run build_runner build --delete-conflicting-outputs # if models/freezed changedflutter build ipa --releaseOutput: build/ios/ipa/*.ipa.
Upload the .ipa to App Store Connect with Transporter, Xcode's Organizer, or
xcrun altool from the command line:
xcrun altool --upload-app --type ios \
-f build/ios/ipa/*.ipa \
--apiKey <API_KEY_ID> \
--apiIssuer <ISSUER_ID>Add your own App Store Connect API credentials — never commit them.
<API_KEY_ID>and<ISSUER_ID>come from App Store Connect → Users and Access → Integrations → App Store Connect API: the key's Key ID and the team's Issuer ID.- Download that key's
AuthKey_<API_KEY_ID>.p8once and place it in~/.appstoreconnect/private_keys/(or~/private_keys/) soaltoolcan find it.- Keep the key ID, issuer ID, and the
.p8file out of the repository (e.g. export them as shell env vars or pass them on the command line at upload time).
flutter build appbundle --releaseOutput: build/app/outputs/bundle/release/app-release.aab. Upload the .aab (App Bundle)
to the Google Play Console — Play prefers App Bundles over APKs. (For local sideloading/testing
only, flutter build apk --release produces a signed .apk.)
-
Android → Google Play Console → create a new release on the target track (internal / closed / production), upload the
.aab, paste the changelog into "What's New". -
iOS → App Store Connect → select the uploaded build, fill release notes, submit for review.
-
Tag the commit so the release is reproducible:
git tag -a v1.2.0 -m "Release 1.2.0" git push origin v1.2.0
-
version:bumped inpubspec.yaml(name and+build, build number increased). - New section added to
CHANGELOG.md. -
android/key.propertiespresent and points to a valid keystore (Android). - Xcode signing/team + App Store Connect record ready (iOS).
-
flutter build ipa --releasesucceeds. -
flutter build appbundle --releasesucceeds. - Artifacts uploaded; release notes match the changelog.
- Commit tagged
vX.Y.Zand pushed.