Skip to content
All posts
April 27, 20266 min read

My Android Release Workflow: From Code to Play Store in Under an Hour

A step-by-step breakdown of how I release Android apps to Google Play as a solo developer — versioning, signing, AAB builds, store listing, and the checklist that prevents me from shipping broken releases.

AndroidPlay StoreReleaseCI/CDAutomationKotlin
Share:

Every Android developer has a release horror story. Wrong keystore. Forgotten version bump. Screenshot from 3 versions ago. App crashing on the first launch for half your users.

After 22+ apps and hundreds of releases, I've reduced that risk to near zero. Here's the exact workflow.


The Release Checklist (Run This Every Time)

Before I touch a build command, I run through this list:

code
PRE-RELEASE CHECKLIST
─────────────────────────────────────
[ ] Version bumped (versionName + versionCode)
[ ] CHANGELOG.md updated with new features/fixes
[ ] All debug logging removed or guarded by BuildConfig.DEBUG
[ ] ProGuard rules verified (no R8 surprises)
[ ] Signing config using local.properties (not hardcoded)
[ ] Target SDK is current (36)
[ ] Permissions in manifest match actual usage
[ ] Privacy policy URL is live
[ ] Store screenshots are current (not from 3 versions ago)
[ ] Release notes written in plain language
[ ] Tested on physical device + emulator (API 24 + API 34)
[ ] Crash-free session rate > 99% on existing version

This isn't a "nice to have." I run it every time. Missing one item has cost me a rollback.


Version Management

All my apps use the same version file:

properties
# config/version.properties
VERSION_MAJOR=1
VERSION_MINOR=4
VERSION_PATCH=0
VERSION_CODE=52

The

code
build.gradle.kts
reads it:

kotlin
import java.util.Properties

val versionProps = Properties().apply {
    load(rootProject.file("config/version.properties").inputStream())
}

android {
    defaultConfig {
        versionName = "${versionProps["VERSION_MAJOR"]}.${versionProps["VERSION_MINOR"]}.${versionProps["VERSION_PATCH"]}"
        versionCode = (versionProps["VERSION_CODE"] as String).toInt()
    }
}

Why this matters: Never hardcode version numbers in

code
build.gradle
. When you have 22 repos, you need a single source of truth per app — not values scattered across Gradle files.

Bumping versions

I use a script that handles the math:

bash
#!/bin/bash
# version-bump.sh patch|minor|major [repo-name]

BUMP_TYPE=${1:-patch}
PROPS_FILE="config/version.properties"

# Read current values
MAJOR=$(grep VERSION_MAJOR $PROPS_FILE | cut -d= -f2)
MINOR=$(grep VERSION_MINOR $PROPS_FILE | cut -d= -f2)
PATCH=$(grep VERSION_PATCH $PROPS_FILE | cut -d= -f2)
CODE=$(grep VERSION_CODE $PROPS_FILE | cut -d= -f2)

# Increment
CODE=$((CODE + 1))
case $BUMP_TYPE in
  major) MAJOR=$((MAJOR + 1)); MINOR=0; PATCH=0 ;;
  minor) MINOR=$((MINOR + 1)); PATCH=0 ;;
  patch) PATCH=$((PATCH + 1)) ;;
esac

# Write back
sed -i '' "s/VERSION_MAJOR=.*/VERSION_MAJOR=$MAJOR/" $PROPS_FILE
sed -i '' "s/VERSION_MINOR=.*/VERSION_MINOR=$MINOR/" $PROPS_FILE
sed -i '' "s/VERSION_PATCH=.*/VERSION_PATCH=$PATCH/" $PROPS_FILE
sed -i '' "s/VERSION_CODE=.*/VERSION_CODE=$CODE/" $PROPS_FILE

echo "Bumped to $MAJOR.$MINOR.$PATCH (code: $CODE)"

Run

code
./version-bump.sh patch
before every release. Never do it manually.


Signing Configuration

This is where most developers make dangerous mistakes. Here's the safe setup:

properties
# local.properties (NEVER commit this file)
KEYSTORE_PATH=/Users/sudarshan/keystores/myfamilytracker.jks
KEYSTORE_PASSWORD=your-password-here
KEY_ALIAS=myfamilytracker
KEY_PASSWORD=your-password-here
kotlin
// build.gradle.kts
import java.util.Properties

val localProps = Properties().apply {
    val f = rootProject.file("local.properties")
    if (f.exists()) load(f.inputStream())
}

android {
    signingConfigs {
        create("play store") {
            storeFile = localProps["KEYSTORE_PATH"]?.let { file(it) }
            storePassword = localProps["KEYSTORE_PASSWORD"] as? String
            keyAlias = localProps["KEY_ALIAS"] as? String
            keyPassword = localProps["KEY_PASSWORD"] as? String
        }
    }
    buildTypes {
        release {
            signingConfig = signingConfigs.getByName("play store")
            isMinifyEnabled = true
            isShrinkResources = true
            proguardFiles(
                getDefaultProguardFile("proguard-android-optimize.txt"),
                "proguard-rules.pro"
            )
        }
    }
}

Critical rule:

code
local.properties
must be in
code
.gitignore
. If you accidentally commit credentials, rotate them immediately — even if the repo is private.


Building the AAB

Always ship an AAB (Android App Bundle), never an APK, to Google Play. AABs let Play Store generate optimized APKs per device. Smaller download size, faster installs, better user retention.

bash
# Clean build to avoid cache issues
./gradlew clean

# Build release AAB
./gradlew bundleRelease

# Output location
ls -la app/build/outputs/bundle/release/app-release.aab

I always do a clean build for releases. Build cache has caused me subtle bugs exactly once — that was enough.

Verifying the AAB

Before uploading, I verify the signing:

bash
# Check the AAB is signed with the right key
bundletool validate --bundle=app/build/outputs/bundle/release/app-release.aab

# Extract APKs locally to test on device
bundletool build-apks \
  --bundle=app/build/outputs/bundle/release/app-release.aab \
  --output=app.apks \
  --ks=myfamilytracker.jks \
  --ks-key-alias=myfamilytracker

bundletool install-apks --apks=app.apks

Play Store Submission

Release Notes

Write release notes in plain language. Not developer speak:

code
❌ "Fixed NPE in LocationService initialization"
✅ "Fixed a crash that some users experienced when first opening the app"

❌ "Migrated from deprecated API to LocationManager v2"  
✅ "Improved location accuracy and battery efficiency"

Your users don't know what an NPE is. They know "the app crashed."

Rollout Strategy

I never release to 100% immediately. My default rollout:

code
Day 1:  10% rollout
Day 2:  Check crash-free rate in Play Console
Day 3:  25% rollout (if stable)
Day 5:  50% rollout
Day 7:  100% rollout

If crash-free rate drops below 99%, I halt and investigate before expanding. Play Console shows you crash data within hours.


Post-Release Monitoring

After every release, I monitor for 48 hours:

  1. Play Console → Android Vitals — crash-free rate, ANR rate
  2. Firebase Crashlytics — real-time crash reports with stack traces
  3. Reviews — new 1-star reviews often point to bugs before analytics catch them
kotlin
// Crashlytics setup in Application class
@HiltAndroidApp
class MyApp : Application() {
    override fun onCreate() {
        super.onCreate()
        if (!BuildConfig.DEBUG) {
            FirebaseCrashlytics.getInstance().setCrashlyticsCollectionEnabled(true)
        }
    }
}

Never enable Crashlytics in debug builds. You'll drown in noise from your own testing.


When Something Goes Wrong

Play Store lets you halt a rollout instantly. If you see a spike in crashes after expanding to 25%:

  1. Halt the rollout immediately in Play Console
  2. Pull the crash report from Crashlytics
  3. Fix, build, and submit a new release
  4. The new release can go to 100% if you've confirmed the fix

I've done emergency rollbacks. The key is catching problems at 10% before they reach everyone.


The Full Timeline

StepTime
Run pre-release checklist10 min
Bump version1 min
Clean + build AAB8-12 min
Verify signing2 min
Install + smoke test10 min
Write release notes5 min
Upload to Play Console5 min
Submit for review2 min
Total~45 min

Google Play review takes 1-3 days for updates (longer for new apps). Plan accordingly — if you're fixing a critical bug, submit as soon as the fix is ready.


One Last Thing

Keep your keystores backed up in at least two places. Losing your keystore means you can never update your app — you'd have to publish it as a completely new app and lose all your reviews, ratings, and download history.

I store keystores in an encrypted backup on two separate drives. Not on GitHub. Not on cloud storage with public access. Offline, encrypted, in two physical locations.

This is the one disaster you can't recover from. Treat your keystores accordingly.

Share:
S

Sudarshan Chaudhari

AI Systems Builder / Product Engineer

Bangkok, Thailand

Solo Android developer with 13+ years in QA, building Android apps, AI automation systems, and developer tools at SudarshanTechLabs.

Stay updated

Get new posts on Android, Kotlin, and solo dev straight to your inbox.

Newsletter preferences

Related Apps

MyFamilyTracker

Real-time family location sharing — Firebase Realtime DB for sub-second propagation, WorkManager + ForegroundService for OS-compliant background collection, geofencing via Google Maps API.

Building something? Available for Android dev and QA consulting.

Work with me

Comments — powered by Giscus

Apps tagged with this