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-not-production-project
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-not-production-project
└── 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 Console → Users 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. Testing → Internal testing → Create 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 → Testing → Internal 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 content → Privacy 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​