Android Build System
This guide explains how the Android build system is structured, including Gradle flavors, signing configurations, Fastlane lanes, and Play Store deployment.
Architecture Overview
Product Flavors
Product flavors allow building different versions of the app with different configurations.
Available Flavors
| Flavor | Package ID | App Name | Firebase Project |
|---|---|---|---|
dev | com.pivot3.dev | Pivot Dev | pivot-dev-59310 |
qa | com.pivot3.qa | Pivot QA | pivot-staging |
prod | com.pivot3 | Pivot app | pivot-inc |
Flavor Configuration
Located in android/app/build.gradle:
productFlavors {
dev {
dimension "default"
applicationId "com.pivot3.dev"
resValue "string", "app_name", "Pivot Dev"
signingConfig signingConfigs.dev
// Only arm64 for faster CI builds
ndk { abiFilters "arm64-v8a" }
}
qa {
dimension "default"
applicationId "com.pivot3.qa"
resValue "string", "app_name", "Pivot QA"
signingConfig signingConfigs.qa
ndk { abiFilters "arm64-v8a" }
}
prod {
dimension "default"
applicationId "com.pivot3"
resValue "string", "app_name", "Pivot app"
signingConfig signingConfigs.release
// All architectures for Play Store
ndk { abiFilters "armeabi-v7a", "arm64-v8a", "x86", "x86_64" }
}
}
Firebase Configuration
Each flavor has its own Firebase config:
android/app/src/
├── dev/google-services.json → pivot-dev-59310
├── qa/google-services.json → pivot-staging
└── prod/google-services.json → pivot-inc
Signing Configuration
Keystore Files
Each environment has a dedicated keystore:
| Environment | Keystore | Key Alias | GitHub Secret |
|---|---|---|---|
| Dev | pivot-dev.keystore | pivot-dev | ANDROID_KEYSTORE_DEV |
| QA | pivot-qa.keystore | pivot-qa | ANDROID_KEYSTORE_QA |
| Production | pivot-release.keystore | pivot-release | ANDROID_KEYSTORE_RELEASE |
Signing Config in Gradle
signingConfigs {
dev {
if (System.getenv("CI")) {
storeFile file("${rootProject.projectDir}/keystores/pivot-dev.keystore")
storePassword System.getenv("ANDROID_KEYSTORE_PASSWORD_DEV")
keyAlias "pivot-dev"
keyPassword System.getenv("ANDROID_KEY_PASSWORD_DEV")
} else if (project.hasProperty('PIVOT_DEV_STORE_FILE')) {
storeFile file(PIVOT_DEV_STORE_FILE)
storePassword PIVOT_DEV_STORE_PASSWORD
keyAlias PIVOT_DEV_KEY_ALIAS
keyPassword PIVOT_DEV_KEY_PASSWORD
}
}
// Similar for qa and release...
}
Creating a New Keystore
keytool -genkeypair -v \
-keystore pivot-newenv.keystore \
-alias pivot-newenv \
-keyalg RSA \
-keysize 2048 \
-validity 10000 \
-storepass YOUR_PASSWORD \
-keypass YOUR_PASSWORD \
-dname "CN=Pivot, OU=Mobile, O=Pivot Inc, L=Montreal, ST=Quebec, C=CA"
To add to GitHub secrets (base64 encode):
base64 -w 0 pivot-newenv.keystore > pivot-newenv.keystore.b64
Fastlane Configuration
Fastlane automates building and uploading to Play Store.
Lane Structure
Lane Definitions
Located in fastlane/Fastfile:
platform :android do
desc "Upload Dev version to Play Store Internal Track"
lane :internal do
upload_to_play_store(
track: "internal",
package_name: "com.pivot3.dev",
json_key: ENV['SUPPLY_JSON_KEY'] || './fastlane/play-store-key.json',
aab: './android/app/build/outputs/bundle/devRelease/app-dev-release.aab',
release_status: "draft",
skip_upload_metadata: true,
skip_upload_images: true,
skip_upload_screenshots: true,
)
end
desc "Upload QA version to Play Store Internal Track"
lane :beta do
upload_to_play_store(
track: "internal",
package_name: "com.pivot3.qa",
aab: './android/app/build/outputs/bundle/qaRelease/app-qa-release.aab',
release_status: "draft",
# ... same options
)
end
desc "Upload Production version to Play Store"
lane :playstore do
upload_to_play_store(
track: "internal",
package_name: "com.pivot3",
aab: './android/app/build/outputs/bundle/prodRelease/app-prod-release.aab',
release_status: "completed",
# ... same options
)
end
end
Release Status
| Status | Use Case |
|---|---|
draft | New apps that haven't completed first production release |
completed | Apps that have been released to production at least once |
New Play Store apps (Pivot Dev, Pivot QA) must use release_status: "draft" until they complete their first full release through the Play Console.
Play Store Service Account
Service Account Details
- Email:
pivot-play-deploy@pivot-inc.iam.gserviceaccount.com - Project: pivot-inc
- Key file:
fastlane/play-store-key.json - Backup:
pivot-devops/github/secrets/play-store-service-account.json - GitHub Secret:
PLAY_STORE_JSON_KEY
Creating a Service Account
- Go to Google Cloud Console
- Click Create Service Account
- Name:
pivot-play-deploy - Create and download JSON key
- In Play Console → Users and permissions
- Click Invite new users
- Add the service account email
- Grant Admin permission
- Add JSON key to GitHub secrets as
PLAY_STORE_JSON_KEY
GitHub Actions Workflows
Workflow Files
Located in .github/workflows/:
| File | Branch Trigger | Fastlane Lane |
|---|---|---|
android-dev-build.yml | development | internal |
android-qa-build.yml | main | beta |
android-production-build.yml | production | playstore |
Reusable Workflow
All builds use android-build.yml which:
- Checks out code
- Sets up Node.js, Java, Ruby
- Installs dependencies
- Caches Gradle and Android build
- Decodes keystore from secrets
- Auto-increments version code
- Builds AAB with Gradle
- Uploads to Play Store via Fastlane
Auto-Increment Version Code
The workflow automatically increments versionCode:
- name: Auto-increment version code
run: |
CURRENT=$(grep -oP 'versionCode \K\d+' android/app/build.gradle)
NEW=$((CURRENT + 1))
sed -i "s/versionCode $CURRENT/versionCode $NEW/" android/app/build.gradle
echo "Version code: $CURRENT → $NEW"
This only increments in CI, not committed back to repo. For major version bumps, update android/app/build.gradle manually.
Setting Up a New Play Store App
When creating a new app (e.g., Pivot Dev 2):
1. Create App in Play Console
- Go to Play Console
- Create app → Enter details
- Set up Internal testing track
2. First AAB Upload (Manual Required)
The first AAB must be uploaded manually - the API cannot create a new app:
# Build locally
cd pivot-mobile/android
./gradlew bundleDevRelease
# Find AAB at:
# android/app/build/outputs/bundle/devRelease/app-dev-release.aab
In Play Console:
- Testing → Internal testing → Create new release
- Upload the AAB manually
- Save as draft
3. Complete App Requirements
Before automated uploads work:
- Privacy Policy: App content → Privacy policy → Add URL
- App Access: If app requires login, provide test credentials
- Content Rating: Complete the questionnaire
- Target Audience: Set age group
4. Automated Uploads Now Work
After first manual upload, CI/CD can upload subsequent versions automatically.
Managing Internal Testers
Google Cloud Identity Group
Testers are managed via a Google Group for CLI automation.
- Group:
pivot-android-testers@pivotapp.ca - Console: https://admin.google.com/ac/groups
CLI Commands
# Add a tester
gcloud identity groups memberships add \
--group-email="pivot-android-testers@pivotapp.ca" \
--member-email="newuser@example.com" \
--roles=MEMBER
# Remove a tester
gcloud identity groups memberships delete \
--group-email="pivot-android-testers@pivotapp.ca" \
--member-email="olduser@example.com"
# List all testers
gcloud identity groups memberships list \
--group-email="pivot-android-testers@pivotapp.ca" \
--format="table(preferredMemberKey.id)"
Linking Group to Play Console
- Play Console → App → Testing → Internal testing
- Under Testers, edit the email list
- Add
pivot-android-testers@pivotapp.ca - Save
Anyone added to the Google Group automatically gets access.
Build Times
| Environment | Approximate Time |
|---|---|
| GitHub Actions | 40-50 minutes |
| Local (first build) | 3-5 minutes |
| Local (cached) | 15-30 seconds |
Build Optimization
Dev and QA builds only target arm64 architecture for faster builds:
ndk { abiFilters "arm64-v8a" }
Production builds include all architectures for Play Store compatibility.
GitHub Secrets
| Secret | Description |
|---|---|
ANDROID_KEYSTORE_DEV | Base64-encoded dev keystore |
ANDROID_KEYSTORE_QA | Base64-encoded QA keystore |
ANDROID_KEYSTORE_RELEASE | Base64-encoded release keystore |
ANDROID_KEYSTORE_PASSWORD_DEV | Dev keystore password |
ANDROID_KEYSTORE_PASSWORD_QA | QA keystore password |
ANDROID_KEYSTORE_PASSWORD_RELEASE | Release keystore password |
PLAY_STORE_JSON_KEY | Play Store service account JSON |
Troubleshooting
"Package not found" Error
Cause: First AAB hasn't been uploaded manually.
Solution: Upload first AAB via Play Console UI. The API cannot create new apps.
"Only releases with status draft may be created"
Cause: App hasn't completed first production release.
Solution: Use release_status: "draft" in Fastlane until app completes full release.
"Privacy policy required"
Cause: App uses permissions requiring a privacy policy (camera, location, etc.).
Solution: In Play Console → App content → Privacy policy → Add URL.
Service Account Permission Denied
Cause: Service account not invited to Play Console.
Solution:
- Play Console → Users and permissions
- Invite service account email
- Grant Admin permissions
Build Timeout
Cause: GitHub Actions has 60-minute timeout.
Solution: Ensure Gradle caching is working. Check cache hit rates in workflow logs.
File Locations
| File | Location |
|---|---|
| GitHub Workflows | pivot-mobile/.github/workflows/ |
| Fastfile | pivot-mobile/fastlane/Fastfile |
| build.gradle | pivot-mobile/android/app/build.gradle |
| Play Store Key | pivot-mobile/fastlane/play-store-key.json |
| Key Backup | pivot-devops/github/secrets/play-store-service-account.json |
Next Steps
- Creating New Environments - Add dev2, dev3, etc.
- iOS Build System - iOS equivalent documentation