Skip to main content

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

FlavorPackage IDApp NameFirebase Project
devcom.pivot3.devPivot Devpivot-dev-59310
qacom.pivot3.qaPivot QApivot-staging
prodcom.pivot3Pivot apppivot-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:

EnvironmentKeystoreKey AliasGitHub Secret
Devpivot-dev.keystorepivot-devANDROID_KEYSTORE_DEV
QApivot-qa.keystorepivot-qaANDROID_KEYSTORE_QA
Productionpivot-release.keystorepivot-releaseANDROID_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

StatusUse Case
draftNew apps that haven't completed first production release
completedApps that have been released to production at least once
warning

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

  1. Go to Google Cloud Console
  2. Click Create Service Account
  3. Name: pivot-play-deploy
  4. Create and download JSON key
  5. In Play ConsoleUsers and permissions
  6. Click Invite new users
  7. Add the service account email
  8. Grant Admin permission
  9. Add JSON key to GitHub secrets as PLAY_STORE_JSON_KEY

GitHub Actions Workflows

Workflow Files

Located in .github/workflows/:

FileBranch TriggerFastlane Lane
android-dev-build.ymldevelopmentinternal
android-qa-build.ymlmainbeta
android-production-build.ymlproductionplaystore

Reusable Workflow

All builds use android-build.yml which:

  1. Checks out code
  2. Sets up Node.js, Java, Ruby
  3. Installs dependencies
  4. Caches Gradle and Android build
  5. Decodes keystore from secrets
  6. Auto-increments version code
  7. Builds AAB with Gradle
  8. 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"
note

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

  1. Go to Play Console
  2. Create app → Enter details
  3. 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:

  1. TestingInternal testingCreate new release
  2. Upload the AAB manually
  3. 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.

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

  1. Play Console → App → TestingInternal testing
  2. Under Testers, edit the email list
  3. Add pivot-android-testers@pivotapp.ca
  4. Save

Anyone added to the Google Group automatically gets access.


Build Times

EnvironmentApproximate Time
GitHub Actions40-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

SecretDescription
ANDROID_KEYSTORE_DEVBase64-encoded dev keystore
ANDROID_KEYSTORE_QABase64-encoded QA keystore
ANDROID_KEYSTORE_RELEASEBase64-encoded release keystore
ANDROID_KEYSTORE_PASSWORD_DEVDev keystore password
ANDROID_KEYSTORE_PASSWORD_QAQA keystore password
ANDROID_KEYSTORE_PASSWORD_RELEASERelease keystore password
PLAY_STORE_JSON_KEYPlay 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 contentPrivacy policy → Add URL.

Service Account Permission Denied

Cause: Service account not invited to Play Console.

Solution:

  1. Play Console → Users and permissions
  2. Invite service account email
  3. 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

FileLocation
GitHub Workflowspivot-mobile/.github/workflows/
Fastfilepivot-mobile/fastlane/Fastfile
build.gradlepivot-mobile/android/app/build.gradle
Play Store Keypivot-mobile/fastlane/play-store-key.json
Key Backuppivot-devops/github/secrets/play-store-service-account.json

Next Steps