All Decisions

ADR-0022: Multi-Stage GitLab CI Pipeline with Firebase Integration

DateFebruary 15, 2026
CategoryInfrastructure
Tags
ci-cdtesting

Context

We need a robust CI/CD pipeline that:

  • Validates code quality and correctness on every push
  • Builds distribution-ready artifacts securely
  • Tests across multiple Android configurations
  • Distributes releases to QA automatically
  • Minimizes pipeline execution time through caching
  • Handles sensitive keystores and service accounts securely
  • Runs on self-hosted infrastructure for cost control

The pipeline must support parallel execution where possible, fail fast on critical issues, and provide clear feedback to developers.

Decision

We will implement a four-stage GitLab CI pipeline with the following architecture:

  1. Prepare Stage: Download secure files (keystores, service account keys) using GitLab Secure Files API
  2. Build Stage: Compile both debug and release APKs in parallel jobs
  3. Validation Stage: Run static analysis, unit tests, and instrumented tests (via Firebase Test Lab) in parallel
  4. Release Stage: Distribute release builds to QA testers via Firebase App Distribution

The pipeline uses:

  • Docker executor with android-sdk:36 base image, running on self-hosted Hetzner runners
  • Gradle caching (wrapper, dependencies, build cache, configuration cache) keyed by wrapper version
  • Secure file handling via GitLab CLI (glab) to inject credentials at runtime
  • Firebase Test Lab for on-device instrumented testing on main branch
  • Firebase App Distribution for automated QA distribution on main branch

Rationale

Multi-stage design allows:

  • Clear separation of concerns (prepare, build, validate, release)
  • Parallel execution within stages (debug/release builds, multiple validation types)
  • Progressive artifact flow (secure files → builds → tests → distribution)
  • Fail-fast behavior (validation failures don't trigger releases)

Secure file isolation via prepare stage:

  • Credentials never stored in repository or CI variables
  • Short-lived artifacts (1 hour) minimize exposure window
  • Separate service accounts for CI (test runner) and CD (distribution)

Firebase integration:

  • Test Lab provides real device testing without maintaining device farms
  • App Distribution streamlines QA feedback loop
  • Both restricted to main branch to conserve quota

Caching strategy:

  • Gradle wrapper and dependencies cached globally
  • Build cache persists incremental compilation state
  • Configuration cache speeds up configuration phase
  • Keyed by wrapper version ensures cache invalidation on Gradle upgrades

Self-hosted runners (tags: ["docker", "hetzner"]):

  • Predictable performance and cost
  • No GitLab SaaS minute consumption
  • Dedicated resources for Android builds (memory-intensive)

Gradle configuration:

  • Daemon disabled (-Dorg.gradle.daemon=false) for CI reproducibility
  • Incremental compilation off (-Dkotlin.incremental=false) to avoid cache corruption
  • File system watching disabled (-Dorg.gradle.vfs.watch=false) in containerized environment
  • 2GB heap default, 4GB for release builds

Consequences

Positive:

  • Fast feedback: lint/test failures visible within minutes
  • Reproducible builds: secure files + configuration cache ensure consistency
  • Scalable testing: Firebase Test Lab handles device matrix easily
  • Automated distribution: QA receives builds immediately on main branch merge
  • Cost control: self-hosted runners + strategic Firebase usage

Known downsides:

  • Self-hosted runners require maintenance (Docker image updates, runner upgrades)
  • Firebase Test Lab limited to main branch (quota concerns)
  • Secure files require manual setup via GitLab UI
  • Cache sizing requires monitoring (.gradle/caches can grow large over time)

Follow-up work:

  • Monitor cache hit rates and tune cache key strategy if needed
  • Consider adding test coverage reporting (JaCoCo)
  • May need ADR for deployment to Play Store (currently manual)
  • Evaluate adding performance testing stage for critical user flows

Alternatives Considered

GitHub Actions:

  • Rejected: I prefer GitLab and don't like GitHub

Single-stage pipeline:

  • Pros: Simpler configuration
  • Cons: No parallelization, unclear failure attribution, wasteful (always builds release even on lint failure)
  • Rejected: Performance and clarity benefits of multi-stage outweigh complexity

Gradle Play Publisher plugin:

  • Pros: Automated Play Store deployment
  • Cons: Requires production service account in CI, higher risk, less manual review
  • Deferred: Not needed until beta/production release cadence increases (will merit separate ADR)

Jenkins:

  • Pros: Maximum flexibility, extensive plugin ecosystem
  • Cons: Maintenance overhead, steeper learning curve, need to self-host web UI + controller
  • Rejected: GitLab CI provides sufficient functionality with lower operational cost

Notes

  • Pipeline configuration uses environment variables ($RELEASE_STORE_PASSWORD, etc.) for runtime secrets
  • Artifact retention varies by stage: 1hr (secure files), 1d (debug APKs), 3d (release APKs), 7d (reports)
  • Firebase Test Lab uses MediumPhone.arm API 36 as representative test device
  • QA group must be configured in Firebase App Distribution console
  • Release builds require FIREBASE_APP_ID CI variable