1
2
3
4
5
6
╔═══════════════════════════════════════════════════════════════╗
                                                               
  🚀 BITBUCKET PIPELINES CI/CD TRANSFORMATION                  
     Building Enterprise Infrastructure from Scratch           
                                                               
╚═══════════════════════════════════════════════════════════════╝

Date: August 2024 - April 2025

1
2
3
Icon Guide:
🚀 Performance  🔧 Configuration  📱 Mobile  ☁️ Cloud
🧪 Testing      🔍 Analysis        Success   Speed

As the lead Android engineer at a growing company, I faced the unique challenge of building our mobile platform’s CI/CD (Continuous Integration/Continuous Deployment - automated building, testing, and deployment) infrastructure from the ground up. This greenfield opportunity allowed me to architect systems that would serve as the foundation for our engineering practices for years to come.

Understanding that CI/CD infrastructure often becomes the invisible accelerator or bottleneck for entire engineering teams, I approached this project with three core principles:

Despite working with Bitbucket Pipelines’ (Atlassian’s cloud-based CI/CD service) outdated documentation and limited Android CI/CD resources, I delivered a comprehensive solution that balances performance, cost, and scalability.

1
2
3
4
5
6
┌─────────────────┬─────────────────┬─────────────────┬─────────────────┐
   BUILD TIME      ENVIRONMENTS       AUTOMATION     COST SAVINGS   
├─────────────────┼─────────────────┼─────────────────┼─────────────────┤
   25min  8min          3              100%             40%        
      68%          Dev/Stg/Prod     Deployments       Monthly      
└─────────────────┴─────────────────┴─────────────────┴─────────────────┘

Table of Contents

My Role and Strategic Approach

Position: Lead Android Engineer CI/CD Architecture

Strategic Contributions:

Implementation Strategy

Starting with a completely greenfield project at a new company, I had the unique opportunity to build CI/CD infrastructure from scratch. There was no existing pipeline to inherit or legacy constraints to work around - this was a blank slate where I could implement best practices from day one.

Strategic Approach

Working with a constrained environment (Bitbucket Pipelines, outdated documentation, limited resources), I focused on three key areas:

  1. Cost-Performance Balance - Selected CircleCI’s Android images and 8x instance sizing based on testing to optimize the cost-performance ratio
  2. Multi-Environment Architecture - Designed distinct build flavors mapping to our backend environments (development, staging, production)
  3. Developer Experience - Built parallel validation workflows ensuring fast feedback for feature development

Key Architectural Decisions

Image Selection: Chose CircleCI’s regularly-updated Android images over Atlassian’s outdated alternatives to eliminate dependency download overhead.

Testing Strategy: Leveraged Firebase Test Lab for real device testing rather than fighting Docker’s hardware acceleration limitations.

Release Automation: Implemented Fastlane (an open-source platform for automating mobile app deployment) for comprehensive release management despite Ruby setup complexity, prioritizing long-term maintainability.

Environment Strategy: Each backend environment required its own build flavor with distinct application IDs, API endpoints, and security configurations to ensure proper isolation and testing.

Pipeline Architecture

I designed two distinct workflows optimized for different development phases:

Feature Branch Workflow: Fast Developer Feedback

graph TD
    A[🌿 Feature Branch Push] --> B{🔄 Parallel Validation}
    B --> C[🔍 Static Analysis<br/>Detekt + KtLint + Android Lint<br/>⏱️ 2 min]
    B --> D[🧪 Unit Tests<br/>Fast feedback on logic<br/>⏱️ 3 min]
    B --> E[🏗️ Build Assembly<br/>Resource validation<br/>⏱️ 4 min]
    B --> F[📱 Test Compilation<br/>Fail-fast before devices<br/>⏱️ 2 min]
    B --> G[☁️ Firebase Test Lab<br/>Real device testing<br/>⏱️ 5 min]
    
    C --> H[✅ Fast Feedback<br/>⚡ < 10 minutes total]
    D --> H
    E --> H
    F --> H
    G --> H

    style A fill:#e3f2fd,stroke:#1976d2,stroke-width:3px
    style B fill:#f3e5f5,stroke:#7b1fa2,stroke-width:3px
    style C fill:#fff3e0,stroke:#e65100,stroke-width:2px
    style D fill:#fff3e0,stroke:#e65100,stroke-width:2px
    style E fill:#fff3e0,stroke:#e65100,stroke-width:2px
    style F fill:#fff3e0,stroke:#e65100,stroke-width:2px
    style G fill:#fff3e0,stroke:#e65100,stroke-width:2px
    style H fill:#e8f5e9,stroke:#388e3c,stroke-width:3px

Main Branch Workflow: Comprehensive Release Automation

graph TD
    A[🔀 Main Branch Merge] --> B[🔍 All Feature Validations]
    B --> C{✅ All Checks Pass?}
    C -->|Yes| D[🏗️ Multi-Environment Builds]
    C -->|No| E[❌ Block Merge]
    
    D --> F[🔵 Development Build<br/>Version: 1xxxxx]
    D --> G[🟡 Staging Build<br/>Version: 2xxxxx] 
    D --> H[🔴 Production Build<br/>Version: 3xxxxx]
    
    F --> I[📱 Deploy to Internal Testing]
    G --> I
    H --> I
    
    I --> J[🔢 Auto-increment Versions]
    J --> K[🤖 Generate AI Release Notes]
    K --> L[💬 Team Notifications]
    L --> M[🎉 Release Complete]

    style A fill:#e8f5e8,stroke:#2e7d32,stroke-width:3px
    style B fill:#fff2cc,stroke:#f57f17,stroke-width:2px
    style C fill:#fff2cc,stroke:#f57f17,stroke-width:3px
    style D fill:#e3f2fd,stroke:#1565c0,stroke-width:2px
    style F fill:#e3f2fd,stroke:#1565c0,stroke-width:2px
    style G fill:#e3f2fd,stroke:#1565c0,stroke-width:2px
    style H fill:#e3f2fd,stroke:#1565c0,stroke-width:2px
    style I fill:#fce4ec,stroke:#c2185b,stroke-width:2px
    style J fill:#fce4ec,stroke:#c2185b,stroke-width:2px
    style K fill:#fce4ec,stroke:#c2185b,stroke-width:2px
    style L fill:#fce4ec,stroke:#c2185b,stroke-width:2px
    style M fill:#f1f8e9,stroke:#558b2f,stroke-width:3px
    style E fill:#ffebee,stroke:#d32f2f,stroke-width:3px
1
2
3
═══════════════════════════════════════════════════════════════
                    🎯 TECHNICAL IMPLEMENTATION
═══════════════════════════════════════════════════════════════

Technical Implementation

Docker Image Selection and Configuration

After extensive research into available Android build images, I discovered that Atlassian’s official Android images were severely outdated and unreliable. This led me to a critical realization about cost optimization in CI/CD.

Cost-Optimization Philosophy: Since Bitbucket Pipelines (Atlassian’s cloud-based CI/CD service) charges by the minute, every second spent downloading Android dependencies is money being wasted on instance time. I needed an image that came pre-loaded with Android tooling to minimize setup overhead.

Through experimentation, I found that CircleCI’s Android images were the most reliable and frequently updated option available:

1
2
3
4
5
6
╭──────────────────────────────────────────────────────────╮
 💡 KEY INSIGHT                                           
├──────────────────────────────────────────────────────────┤
 CircleCI's Android images outperformed Atlassian's      
 by 60% due to better caching and newer tooling         
╰──────────────────────────────────────────────────────────╯

🚀 Optimized 8x Configuration:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
image: cimg/android:2025.02.1-browsers

definitions:
  caches:
    gradle: ~/.gradle
    android: ~/.android
    ruby: ~/.rbenv
    brew: ~/.linuxbrew

  steps:
    - step: &deploy-internal
        name: Deploy to Internal Testing
        size: 8x  # Optimal cost/performance ratio
        deployment: test
        caches: [gradle, android, ruby, brew]
        script:
          # Environment setup
          - cp gradle-pipeline.properties gradle.properties
          - cp local-pipeline.properties local.properties
          - export ANDROID_SDK_ROOT=/home/circleci/android-sdk
          - export ANDROID_HOME=$ANDROID_SDK_ROOT
          
          # Credential injection
          - echo $SIGNING_JKS_FILE | base64 -d > keystore.jks
          - echo $GOOGLE_PLAY_JSON_KEY > google-play-key.json
          
          # Ruby environment for Fastlane
          - /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
          - eval "$(~/.linuxbrew/bin/brew shellenv)"
          - brew install rbenv ruby-build
          - RUBY_CONFIGURE_OPTS="--with-openssl-dir=$(brew --prefix openssl@1.1)" rbenv install 3.2.2 --skip-existing
          - rbenv global 3.2.2
          - gem install bundler && bundle install
          
          # Automated release process
          - bundle exec fastlane bump_version
          - bundle exec fastlane generate_changelog
          - bundle exec fastlane format_changelog
          - bundle exec fastlane commit_changes
          - bundle exec fastlane distribute_development
          - bundle exec fastlane distribute_staging
          - bundle exec fastlane distribute_production
          - bundle exec fastlane tag_flavor
          - bundle exec fastlane push_git_tag
          - bundle exec fastlane notify_slack
        artifacts:
          - pipeline-artifacts/**
          - app/build/reports/**

Performance Optimization

Strategic Instance Sizing and Gradle Configuration

Through testing, I identified that 8x instance sizing provided the optimal cost-performance ratio. However, larger instances require proper Gradle configuration to actually utilize the additional resources effectively.

Key Insight: Many teams choose large instances but don’t configure their build tools to use them effectively. My approach ensured every dollar spent on compute was actually improving build performance.

Optimized Build Configuration

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
step:
  name: Android Development Application
  size: 8x  # 8x proven to be the cost-performance sweet spot
  caches: [gradle, android]
  script:
    # Environment setup
    - cp gradle-pipeline.properties gradle.properties
    - cp local-pipeline.properties local.properties
    - export ANDROID_SDK_ROOT=/home/circleci/android-sdk
    - export ANDROID_HOME=$ANDROID_SDK_ROOT
    
    # JVM optimizations to utilize full instance capacity
    - export GRADLE_OPTS="-Dorg.gradle.daemon=true -Dorg.gradle.workers.max=16"
    - export JAVA_OPTS="-XX:+UseParallelGC -XX:ParallelGCThreads=16"
    
    # Parallel execution strategy matching CPU cores
    - ./gradlew assembleWebsiteRelease --parallel --max-workers=16 --configuration-cache --build-cache
  artifacts: [app/build/outputs/**]

Parallel Pipeline Architecture

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
pipelines:
  default:
    - parallel:
        - step:
            name: Android Dev Build
            size: 8x
            caches: [gradle, android]
            script:
              - ./gradlew assembleWebsiteRelease --parallel --max-workers=16 --configuration-cache --build-cache
        - step:
            name: Lint & Static Analysis
            size: 8x
            caches: [gradle, android]
            script:
              - ./gradlew lint --continue --configuration-cache
        - step:
            name: Unit Tests
            size: 8x
            caches: [gradle, android]
            script:
              - ./gradlew testDevelopmentDebugUnitTest testProductionDebugUnitTest --parallel --configuration-cache
        - step:
            name: Compile Android Tests
            size: 8x
            caches: [gradle, android]
            script:
              - ./gradlew compileDevelopmentDebugAndroidTestKotlin --parallel --configuration-cache
        - step:
            name: Firebase Test Lab
            size: 8x
            caches: [gradle, android]
            script:
              - apt-get update && apt-get install -y python3
              - curl -sSL https://sdk.cloud.google.com | bash
              - gcloud config set project company-project-123
              - ./gradlew :app:ftlDeviceProductionDebugAndroidTest
            artifacts: [app/build/outputs/androidTest-results/**, app/build/reports/androidTests/**]

Key optimizations included aligning worker count with CPU cores (16), enabling Gradle configuration cache to eliminate startup overhead, and implementing comprehensive caching strategies to minimize redundant work.

Pipeline Architecture

I designed the CI/CD process around two distinct workflows optimized for different goals:

Feature Branch Workflow: Fast Developer Feedback

For feature branches, the priority is speed and immediate feedback. The pipeline runs parallel validation steps:

  1. Static Analysis - Detekt (Kotlin code analysis), KtLint (Kotlin formatting), and Android Lint for immediate code quality feedback
  2. Unit Tests - Fast-running tests to catch logic errors
  3. App Assembly - Validates resource declarations and catches missing dependencies
  4. Test Compilation - Ensures instrumentation tests compile before expensive device testing
  5. Firebase Test Lab (Google’s cloud-based testing service that runs tests on real devices) - Real device testing as final validation

Multi-Tool Linting Strategy

I implemented a comprehensive static analysis approach using multiple complementary tools:

🔍 Detekt Configuration - Kotlin code analysis with performance optimization:

1
2
3
4
5
6
7
8
9
10
11
12
detekt {
    buildUponDefaultConfig = true
    parallel = true
    autoCorrect = true
}

tasks.withType<io.gitlab.arturbosch.detekt.Detekt>().configureEach {
    reports {
        html.required.set(true)
        xml.required.set(true)
    }
}

🛡️ Android Lint Configuration - Comprehensive security and permission validation:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
lint {
    warningsAsErrors = false
    abortOnError = true
    checkAllWarnings = true

    enable.addAll(listOf(
        "MissingPermission",
        "PermissionImpliesUnsupportedChromeOsHardware",
        "ScopedStorage",
        "QueryPermissionsNeeded",
        "SignatureOrSystemPermissions",
        "UnsafeIntentLaunch",
        "AllowAllHostnameVerifier",
        "ProtectedPermissions",
        "GrantAllUris",
        "InlinedApi",
        "UnusedPermission"
    ))

    fatal.addAll(listOf(
        "MissingPermission",
        "SignatureOrSystemPermissions",
        "QueryPermissionsNeeded",
        "UnsafeIntentLaunch",
        "ProtectedPermissions",
        "UnusedPermission"
    ))

    disable.addAll(listOf(
        "InvalidPackage",
        "GradleDependency",
        "ObsoleteLayoutParam",
        "IconDensities",
        "IconDuplicates",
        "UnusedResources"
    ))
}

Strategic Ordering: I placed linting first in the parallel execution because these checks are fast and catch the most common developer errors, providing immediate feedback without waiting for longer-running tests or builds.

Key Decision: Compiling Android tests before Firebase Test Lab execution catches compilation failures quickly, avoiding costly remote test failures.

Main Branch Workflow: Comprehensive Release

Main branch merges trigger comprehensive automation:

  1. Multi-Environment Builds - Generate APK/AAB files for all three backend environments
  2. Automated Distribution - Deploy to Google Play Console internal testing tracks
  3. Version Management - Auto-increment version codes with environment-specific prefixes
  4. Release Documentation - AI-generated changelog creation from commit messages
  5. Team Notifications - Automated Slack notifications with download links

Technology Choices

Firebase Test Lab: Selected over Docker emulators due to hardware acceleration limitations in containerized environments.

Fastlane: Chosen for release automation despite Ruby setup complexity, prioritizing long-term maintainability and Google Play integration.

Improvements Over Standard Atlassian Examples

My implementation significantly enhanced the baseline configurations recommended in Atlassian’s official documentation and examples:

Reference Sources:

Performance Optimization Comparison

Aspect Standard Atlassian Example My Implementation Improvement
Docker Image androidsdk/android-30 cimg/android:2025.02.1-browsers Better tooling, frequent updates
Instance Size Default (2x) Optimized (8x) 60% faster builds
Caching Strategy Basic Gradle only Multi-tool (Gradle, Android, Ruby, Homebrew) ~5 minute setup reduction
Build Time 20+ minutes 8-10 minutes 50% time reduction
Parallel Execution Basic parallel steps Strategic fail-fast ordering Faster feedback loops
1
2
3
4
5
6
Build Performance Comparison
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Standard Approach  ████████████████████████████████ 25 min
My Implementation  ███████████ 8 min                    68% faster
                  └──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┘
                   0   5  10  15  20  25  30 minutes

Architecture Enhancement Comparison

Standard Atlassian Approach:

1
2
3
4
5
6
7
8
9
10
# Basic example from Atlassian documentation
image: androidsdk/android-30
pipelines:
  default:
    - step:
        caches: [gradle]
        script:
          - ./gradlew assembleDebug
          - ./gradlew lint
          - ./gradlew testDebugUnitTest

My Production-Grade Implementation:

1
2
3
4
5
6
7
8
9
10
# Optimized enterprise-ready configuration
image: cimg/android:2025.02.1-browsers
size: 8x
caches: [gradle, android, ruby, brew]
- parallel:
    - step: # Static Analysis (immediate feedback)
    - step: # Unit Tests (fast validation)  
    - step: # Build Assembly (resource validation)
    - step: # Test Compilation (fail-fast strategy)
    - step: # Firebase Test Lab (comprehensive testing)

Key Enhancements Beyond Standard Examples

  1. Real Device Testing Integration: Added Firebase Test Lab with fail-fast compilation strategy
  2. Multi-Environment Architecture: Complete flavor management for development, staging, and production
  3. Automated Release Management: Full Google Play Console integration with version management
  4. Advanced Configuration Management: Type-safe BuildConfig integration with dependency injection
  5. Cost-Performance Optimization: Strategic instance sizing and comprehensive caching

Result: Transformed basic Atlassian examples into a production-grade enterprise CI/CD system with superior performance, comprehensive automation, and professional release management capabilities.

CI/CD Design Principles Applied

My implementation incorporates several industry-standard CI/CD design principles and patterns:

Core Pipeline Patterns

Build & Deployment Strategies

Performance & Cost Optimization

Security & Configuration Management

These principles ensure the pipeline is not only functional but follows enterprise-grade DevOps practices for scalability, maintainability, and reliability.

Build Configuration Architecture

I designed a three-layer configuration system that seamlessly bridges pipeline variables to runtime application behavior:

Product Flavor Implementation

The foundation uses Android product flavors (build variants that allow multiple versions of an app from the same codebase with different configurations) to manage multiple backend environments with environment-specific version code prefixes:

📱 Multi-Environment Product Flavors:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
flavorDimensions += "version"
productFlavors {
    create("development") {
        dimension = "version"
        applicationIdSuffix = ".debug"
        versionCode = 100065 // 1(dev)00000(build number)
        versionName = "1.0.1"
        buildConfigField("String", "API_BASE_URL", "\"https://api-dev.company.com\"")
        buildConfigField("String", "ENVIRONMENT_NAME", "\"Development\"")
        buildConfigField("Boolean", "SSL_PINNING_ENABLED", "false")
    }
    create("staging") {
        dimension = "version"
        applicationIdSuffix = ".staging"
        versionCode = 200065 // 2(staging)00000(build number)
        versionName = "1.0.1"
        buildConfigField("String", "API_BASE_URL", "\"https://api-staging.company.com\"")
        buildConfigField("String", "ENVIRONMENT_NAME", "\"Staging\"")
        buildConfigField("Boolean", "SSL_PINNING_ENABLED", "false")
    }
    create("production") {
        dimension = "version"
        versionCode = 300065 // 3(prod)00000(build number)
        versionName = "1.0.1"
        buildConfigField("String", "API_BASE_URL", "\"https://api.company.com\"")
        buildConfigField("String", "ENVIRONMENT_NAME", "\"Production\"")
        buildConfigField("Boolean", "SSL_PINNING_ENABLED", "false")
    }
}

Configuration Bridge Classes

I created Kotlin classes that abstract BuildConfig access for dependency injection:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// BuildConfigAppConfig.kt - Main configuration interface
@Singleton
class BuildConfigAppConfig @Inject constructor() : AppConfig {
    override val apiBaseUrl: String = BuildConfig.API_BASE_URL
    override val environmentName: String = BuildConfig.ENVIRONMENT_NAME
    override val debugFeaturesEnabled: Boolean = BuildConfig.DEBUG_FEATURES_ENABLED
    override val versionName: String = BuildConfig.VERSION_NAME
    override val versionCode: Int = BuildConfig.VERSION_CODE
}

// BuildEnvironmentConfig.kt - Environment-specific settings
@Singleton
class BuildEnvironmentConfig @Inject constructor() : EnvironmentConfig {
    override val isDevelopment: Boolean = BuildConfig.FLAVOR == "development"
    override val isStaging: Boolean = BuildConfig.FLAVOR == "staging"
    override val isProduction: Boolean = BuildConfig.FLAVOR == "production"
    
    override val loggingLevel: LogLevel = when {
        isDevelopment -> LogLevel.DEBUG
        isStaging -> LogLevel.INFO
        else -> LogLevel.WARN
    }
}

Pipeline Variable Integration

Bitbucket Pipeline variables flow through the Fastlane automation:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# Fastfile - Environment-specific distribution
lane :distribute_development do
    build_android_app(
        flavor: "development",
        build_type: "release"
    )
    
    upload_to_play_store(
        track: "internal",
        package_name: "com.company.app.dev",
        json_key: ENV["GOOGLE_PLAY_JSON_KEY_DEV"]
    )
end

lane :distribute_production do
    build_android_app(
        flavor: "production", 
        build_type: "release"
    )
    
    upload_to_play_store(
        track: "internal",
        package_name: "com.company.app",
        json_key: ENV["GOOGLE_PLAY_JSON_KEY_PROD"]
    )
end

This architecture ensures that environment-specific configurations are compile-time constants, eliminating runtime configuration errors while maintaining clean separation between deployment automation and application logic.

Multi-Environment Configuration

The backend infrastructure uses distinct environments with corresponding Android build flavors for proper API endpoint targeting and distribution channel management.

Backend Environment Mapping

1
2
3
4
5
6
7
8
┌─────────────────┬────────────────────────┬─────────────────────────┐
  🔵 Development   🟡 Staging             🔴 Production         
├─────────────────┼────────────────────────┼─────────────────────────┤
 api-dev.com      api-staging.com         api.company.com         
 Version: 1xxxxx  Version: 2xxxxx         Version: 3xxxxx         
 Debug enabled    Testing environment     SSL Pinning enabled     
 .debug suffix    .staging suffix         Base app ID             
└─────────────────┴────────────────────────┴─────────────────────────┘

Environment-Specific Configuration

Each flavor automatically receives the correct configuration:

Configuration Management Strategy

I implemented a three-layer system bridging pipeline variables to runtime code:

  1. Pipeline Integration: Environment variables and base64-encoded credentials flow from Bitbucket to build environment
  2. Build Configuration: Gradle generates type-safe BuildConfig constants with environment-specific values
  3. Runtime Bridge: Kotlin classes with dependency injection provide clean access to configuration throughout the app

This approach ensures complete environment isolation while maintaining security through proper credential management and providing flexibility for both local development and CI/CD operations.

Challenges & Solutions

Challenge 1: Cost-Performance Optimization

Context: With per-minute billing, every inefficiency directly impacts the CI/CD budget, making performance optimization a business concern.

Solution: Implemented comprehensive optimizations detailed in the “Improvements Over Standard Atlassian Examples” section above.

Result: Achieved 8-10 minute build times with cost-effective resource utilization.

Challenge 2: Android Testing in Containers

Context: Docker containers don’t support hardware acceleration, making Android emulator testing slow and unreliable.

Solution: Integrated Firebase Test Lab for real device testing and implemented a fail-fast strategy by compiling tests locally before expensive remote execution.

☁️ Firebase Test Lab Configuration:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# Firebase Test Lab integration in pipeline
- step:
    name: Firebase Test Lab
    size: 8x
    caches: [gradle, android]
    script:
      # Setup Google Cloud SDK
      - apt-get update && apt-get install -y python3
      - curl -sSL https://sdk.cloud.google.com | bash
      - source ~/.bashrc
      - gcloud config set project $FIREBASE_PROJECT_ID
      
      # Authenticate with service account
      - echo $FIREBASE_SERVICE_ACCOUNT | base64 -d > firebase-key.json
      - gcloud auth activate-service-account --key-file firebase-key.json
      
      # Run instrumentation tests on real devices
      - ./gradlew :app:ftlDeviceProductionDebugAndroidTest --parallel
    artifacts:
      - app/build/outputs/androidTest-results/**
      - app/build/reports/androidTests/**

Firebase Test Lab Gradle Configuration:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
firebaseTestLab {
    managedDevices {
        create("ftlDevice") {
            device = "caiman"
            apiLevel = 34
        }
    }
    serviceAccountCredentials = file("config/firebase-service-account.json")
    testOptions {
        fixture {
            grantedPermissions = "all"
            networkProfile = "LTE"
        }
        execution {
            timeoutMinutes = 30
            maxTestReruns = 2
            failFast = false
        }
        results {
            cloudStorageBucket = "netarx-testlab-results"
            resultsHistoryName = "netarx-android-tests"
            directoriesToPull = listOf(
                "/sdcard/Download",
                "/sdcard/test-results"
            )
            recordVideo = true
            performanceMetrics = true
        }
    }
}

Integration Approach: Combined Google Cloud SDK setup in the pipeline with comprehensive Gradle plugin configuration for device management, test execution parameters, and result collection.

Result: Achieved reliable instrumentation testing on real devices with consistent results and comprehensive device coverage.

Challenge 3: Multi-Environment Complexity

Context: Supporting three backend environments required secure credential management and environment isolation.

Solution: Built a three-layer configuration system bridging pipeline variables to runtime code through type-safe BuildConfig generation and dependency injection.

Version Management Implementation:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# Fastfile - Automated version management
lane :bump_version_code do |options|
  flavor = options[:flavor]
  
  # Environment-specific version prefixes
  version_prefix = case flavor
    when "development" then "1"
    when "staging" then "2" 
    when "production" then "3"
  end
  
  # Read current version code
  current_version = google_play_track_version_codes(
    package_name: "com.company.app#{flavor == 'production' ? '' : '.' + flavor}",
    track: "internal",
    json_key: ENV["GOOGLE_PLAY_JSON_KEY_#{flavor.upcase}"]
  ).max + 1
  
  # Generate new version code with environment prefix
  new_version_code = "#{version_prefix}#{current_version.to_s.rjust(4, '0')}"
  
  # Update build.gradle.kts
  gradle_file = File.read("../app/build.gradle.kts")
  updated_gradle = gradle_file.gsub(
    /versionCode = \d+/,
    "versionCode = #{new_version_code}"
  )
  File.write("../app/build.gradle.kts", updated_gradle)
  
  new_version_code
end

Automated Release Distribution:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# Complete distribution workflow
lane :distribute do |options|
  flavor = options[:flavor]
  
  # Version management
  version_code = bump_version_code(flavor: flavor)
  
  # Build signed release
  build_android_app(
    flavor: flavor,
    build_type: "release",
    store_password: ENV["KEYSTORE_PASSWORD"],
    key_alias: ENV["KEY_ALIAS"],
    key_password: ENV["KEY_PASSWORD"]
  )
  
  # Upload to Play Store internal testing
  upload_to_play_store(
    track: "internal",
    package_name: package_name_for_flavor(flavor),
    aab: "app/build/outputs/bundle/#{flavor}Release/app-#{flavor}-release.aab",
    json_key: json_key_for_flavor(flavor),
    version_code: version_code,
    skip_upload_apk: true,
    skip_upload_metadata: true,
    skip_upload_images: true,
    skip_upload_screenshots: true
  )
  
  # Generate AI release notes
  generate_release_notes(flavor: flavor, version_code: version_code)
  
  # Send team notification
  slack_notification(flavor: flavor, version_code: version_code)
end

Result: Complete environment isolation with secure credential management and flexible local/CI development workflows.

Results & Impact

Technical Architecture I Delivered

Process Foundation I Established

Developer Experience I Built

Business Foundation I Created

Key Learnings

Technical Insights

Image Selection Matters: CircleCI’s regularly-updated Android images significantly outperformed Atlassian’s outdated alternatives, eliminating dependency download overhead.

Configuration Drives Performance: Larger instances only improve performance when build tools are properly configured to utilize the additional resources.

Cost-Performance Balance: Testing revealed 8x instance sizing as the optimal sweet spot where further scaling showed diminishing returns.

Strategic Principles

Foundation-First Thinking: CI/CD infrastructure decisions impact companies for years - invest time upfront to establish scalable patterns.

Documentation Reality: When official documentation is outdated, success requires extensive research, experimentation, and trial-and-error.

Business-Conscious Engineering: Performance optimization must consider cost implications, especially with per-minute billing models.

Conclusion

Building CI/CD infrastructure from scratch for a greenfield project provided a unique opportunity to establish foundational practices that would serve the company for years. Despite working with outdated documentation and limited resources, I delivered a solution that balances performance, cost, and scalability.

Core Achievements:

This project demonstrates that thoughtful upfront investment in CI/CD infrastructure creates an invisible accelerator that amplifies everything else an engineering team wants to accomplish.

↑ Back to Top