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.
How I replaced every manual build, test, and upload step with a GitHub Actions workflow that deploys to the Play Store on a single push.
On this page
I shipped 22+ apps and still spent every release day clicking buttons in Android Studio. Then I automated the entire pipeline and never looked back.
If you're a solo Android developer managing multiple apps, manual builds eat hours you don't have. One misconfigured signing config and you're debugging keystores at midnight. I built a GitHub Actions workflow that builds, tests, lint-checks, and deploys to the Play Store on every push to main. Here's exactly how it works.
My workflow runs on every push to the main branch. It does four things in sequence:
Before I built this, a release day looked like: open Android Studio, wait for Gradle sync, click Build Bundle, sign manually, open Play Console, upload, wait. That's 45 minutes of my day gone. Now it happens while I'm sleeping.
Here's the full workflow:
name: Android CI/CD
on:
push:
branches: [main]
workflow_dispatch:
env:
SCORAGE_BUCKET: sudarshan-tech-labs-releases
PLAY_STORE_TRACK: internal
jobs:
build-and-deploy:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
cache: gradle
- name: Run lint and tests
run: ./gradlew lintDebug testDebugUnitTest --no-daemon
- name: Build AAB
run: ./gradlew bundleRelease --no-daemon
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: app-release
path: app/build/outputs/bundle/release/app-release.aab
- name: Deploy to Play Store
if: github.ref == 'refs/heads/main'
uses: r0adkll/upload-google-play@v1
with:
serviceAccountJsonPlainText: ${{ secrets.PLAY_STORE_SERVICE_ACCOUNT }}
packageName: com.sudarshantechlabs.appname
releaseFiles: app/build/outputs/bundle/release/app-release.aab
track: ${{ env.PLAY_STORE_TRACK }}
rollout: '0.10'[!IMPORTANT] Store your Play Store service account JSON in GitHub Secrets. Never commit it. Ever.
CI catches things I used to find on-device at 2 AM. Lint failures, test regressions, ProGuard rule conflicts — all caught before they reach a user.
I added a matrix build for my multi-module setup. Most of my apps share a common module, so I test that module independently before building the app module.
// build.gradle.kts - test matrix configuration
tasks.register("testAllModules") {
dependsOn(":common:lintDebug")
dependsOn(":common:testDebugUnitTest")
dependsOn(":app:lintDebug")
dependsOn(":app:testDebugUnitTest")
dependsOn(":app:bundleRelease")
}I map this task in my GitHub Actions step so the full test suite runs before any bundle is created.
- name: Run full test matrix
run: ./gradlew testAllModules --no-daemonHere's what the output looks like when something fails:
> Task :app:testDebugUnitTest FAILED
com.sudarshantechlabs.appname.core.TestAuthFlow > login_with_invalid_creds FAILED
org.gradle.api.GradleException: Test failedI get this email before I even open my laptop. That's the point.
[!WARNING] Don't skip instrumentation tests in CI. UI bugs are your most expensive bugs. Running them catches regressions in navigation, screen rotation, and configuration changes that unit tests miss entirely.
The
r0adkll/upload-google-play# Create service account via gcloud
gcloud iam service-accounts create play-store-bot \
--display-name "Play Store Deploy Bot"
gcloud projects add-iam-policy-binding YOUR_PROJECT_ID \
--member="serviceAccount:play-store-bot@YOUR_PROJECT.iam.gserviceaccount.com" \
--role="roles/serviceusage.serviceUsageConsumer"
# Grant role in Play Console
# Service Accounts & APIs > Grant access > Release ManagerOnce that's done, the action handles upload, rollout percentage, and track targeting. I deploy to internal track first, then promote to production after manual review.
| Step | Before CI/CD | After CI/CD |
|---|---|---|
| Build | 12 min manual | 8 min automated |
| Lint check | Missed half the time | Every push |
| Test run | Local only | GitHub-hosted on every push |
| Play Store upload | Manual drag-and-drop | API call, < 2 min |
| Rollback | Manual | Re-deploy previous tag |
The numbers aren't theoretical. Over the last 6 months, this pipeline saved me roughly 20 hours of release-day overhead across 4 apps.
[!TIP] Pin your Gradle wrapper versions in CI. A Gradle upgrade that changes caching behavior can silently break build times. Lock it with
in your setup-java step.code--gradle-version
I use a
signingConfigsbuild.gradle.ktsandroid {
signingConfigs {
create("play store") {
val keystoreUser = System.getenv("KEYSTORE_USER") ?: ""
val keystorePass = System.getenv("KEYSTORE_PASS") ?: ""
val keyAlias = System.getenv("KEY_ALIAS") ?: ""
val keyPass = System.getenv("KEY_PASSWORD") ?: ""
storeUser = keystoreUser
storePassword = keystorePass
keyAlias = keyAlias
keyPassword = keyPass
}
}
buildTypes {
release {
signingConfig = signingConfigs.getByName("play store")
isMinifyEnabled = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
}[!NOTE] I keep three signing configs: one for debug, one for release-internal, and one for production. The CI workflow uses the internal config by default and promotes to production only on a tagged release.
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.
Related Posts
Related Apps
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 meComments — powered by Giscus