androidengineers.Book a session

CI/CD Pipeline

Signing, Versioning & Build Variants

article20 minMedium

Proper signing, versioning, and build variants are prerequisites for a professional release pipeline. Getting them wrong means failed Play Store submissions or production builds with debug configs.

Signing Configuration

Secure Key Handling in CI

Never commit the keystore to source control. Store secrets in CI environment variables:

// app/build.gradle.kts
android {
    signingConfigs {
        create("release") {
            // Read from environment variables — injected by CI
            storeFile = file(System.getenv("KEYSTORE_PATH") ?: "debug.keystore")
            storePassword = System.getenv("KEYSTORE_PASSWORD") ?: ""
            keyAlias = System.getenv("KEY_ALIAS") ?: ""
            keyPassword = System.getenv("KEY_PASSWORD") ?: ""
        }
    }

    buildTypes {
        release {
            signingConfig = signingConfigs.getByName("release")
            isMinifyEnabled = true
            proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
        }
    }
}
# GitHub Actions: decode base64-encoded keystore from secret
- name: Decode keystore
  run: |
    echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 -d > app/release.keystore

- name: Build release APK
  env:
    KEYSTORE_PATH: release.keystore
    KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
    KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
    KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
  run: ./gradlew bundleRelease

Versioning Strategy

// app/build.gradle.kts
android {
    defaultConfig {
        // versionCode: monotonically increasing integer
        // Use timestamp for CI builds: YYYYMMDDNN (e.g., 2024030401)
        versionCode = System.getenv("VERSION_CODE")?.toInt() ?: 1

        // versionName: semantic version from git tag
        versionName = System.getenv("VERSION_NAME") ?: "1.0.0-dev"
    }
}
# Derive version from git tag (e.g., v2.3.1)
- name: Set version
  run: |
    TAG=${GITHUB_REF#refs/tags/v}
    echo "VERSION_NAME=$TAG" >> $GITHUB_ENV
    # versionCode: date-based YYYYMMDDNN
    echo "VERSION_CODE=$(date +%Y%m%d)01" >> $GITHUB_ENV

Build Variants

android {
    flavorDimensions("environment")

    productFlavors {
        create("dev") {
            dimension = "environment"
            applicationIdSuffix = ".dev"
            versionNameSuffix = "-dev"
            buildConfigField("String", "API_BASE_URL", "\"https://dev-api.example.com\"")
            buildConfigField("Boolean", "ENABLE_LOGGING", "true")
        }

        create("staging") {
            dimension = "environment"
            applicationIdSuffix = ".staging"
            versionNameSuffix = "-staging"
            buildConfigField("String", "API_BASE_URL", "\"https://staging-api.example.com\"")
            buildConfigField("Boolean", "ENABLE_LOGGING", "true")
        }

        create("production") {
            dimension = "environment"
            buildConfigField("String", "API_BASE_URL", "\"https://api.example.com\"")
            buildConfigField("Boolean", "ENABLE_LOGGING", "false")
        }
    }

    buildTypes {
        debug {
            isDebuggable = true
            isMinifyEnabled = false
        }
        release {
            isDebuggable = false
            isMinifyEnabled = true
        }
    }
}

This creates 6 variants: devDebug, devRelease, stagingDebug, stagingRelease, productionDebug, productionRelease.

Using BuildConfig

// Usage in code
class NetworkConfig {
    val baseUrl: String = BuildConfig.API_BASE_URL
    val loggingEnabled: Boolean = BuildConfig.ENABLE_LOGGING
}

Build Variant Source Sets

app/src/
├── main/           shared across all variants
├── dev/            dev-specific resources/code
├── staging/        staging-specific
└── production/     prod-specific (e.g., Firebase google-services.json)
// src/dev/kotlin/com/myapp/FlipperInitializer.kt
object FlipperInitializer {
    fun init(context: Context) { Flipper.start(context) }
}

// src/production/kotlin/com/myapp/FlipperInitializer.kt
object FlipperInitializer {
    fun init(context: Context) { }  // No-op in production
}

Key Takeaways

PracticeRule
Keystore in CIBase64-encode → CI secret → decode in pipeline; never commit keystore
versionCodeAlways incrementing; use date-based format for CI traceability
versionNameDerived from git tag at release time
Product flavorsSeparate applicationId per environment so they install side-by-side
BuildConfig fieldsInject environment URLs/flags; never use if (BuildConfig.DEBUG) for production feature flags
Source setsSwap entire implementations (Flipper, analytics) per variant, not if statements

YOUR LEARNING JOURNEY

0 of 177 available lessons completed

Progress saved in this browser. No account needed.
Signing, Versioning & Build Variants | Android System Design | Android Engineers