ADR-0022: Multi-Stage GitLab CI Pipeline with Firebase Integration
DateFebruary 15, 2026
CategoryInfrastructure
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:
- Prepare Stage: Download secure files (keystores, service account keys) using GitLab Secure Files API
- Build Stage: Compile both debug and release APKs in parallel jobs
- Validation Stage: Run static analysis, unit tests, and instrumented tests (via Firebase Test Lab) in parallel
- 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