Skip to content

Mobile App CI/CD Pipeline

Here's a breakdown of the Coldtivate app's GitLab CI/CD pipeline, the one used for building and deploying the React Native project. It gets into the technical details, but also explains why things are set up the way they are.

The pipeline aims to deliver:

  • Efficiency: Less manual work, more automation, faster development.
  • Consistency: Making sure builds work the same way every time, no matter who's doing it or where.
  • Security: Keeping code and releases safe from unauthorized access.
  • Transparency: Clear pipelines anyone can understand and use.

A Step-by-Step Breakdown

graph LR
    A[Start] --> B[GitLab CI Pipeline];
    B --> C[Dependencies];
    B --> K[Validation];
    C --> D[Build CI Image];
    C --> E[Fastlane];
    K --> E;
    E --> F[Android Build];
    E --> G[iOS Build];
    F --> H[Play Store];
    G --> I[App Store];

    subgraph "CI Stages"
        C;
        K;
        E;
    end

    subgraph "Distribution"
        F;
        G;
        H;
        I;
    end

The pipeline follows code from development to users through three stages:

  1. Dependencies: Laying the Foundation

    • Sets up the build environment by installing all required dependencies.
    • Uses a custom Docker image (${CI_REGISTRY_IMAGE}:ci) that's updated when the docker/ci directory changes, ensuring consistent tools for everyone.
    • Handles JavaScript and Ruby dependencies with Yarn and Bundler.
    • Manages secrets with Git secret, using a GPG key for decryption.
  2. Build: Crafting the Application

    • This stage transforms code into deployable Android and iOS apps.
    • Android builds use Gradle and Fastlane for automation.
    • iOS builds rely on Xcode, CocoaPods, and Fastlane.
    • Fastlane picks the right .env file and generates build numbers.
    • Build caching speeds up the process.
  3. Deploy: Reaching Our Users

    • Final stage: deploying apps to Google Play Store and TestFlight.
    • Fastlane automates deployment for reliable releases.
    • Manual triggers allow controlled rollout when ready.

Fastlane

Fastlane powers all the automation. It handles complex mobile build and deployment tasks while keeping things simple. The pipeline uses it to:

  • Setting up environment configs with setup_dotenv.
  • Creating build numbers using generate_build_id and parse_version_id.
  • Pushing Android apps to Play Store via upload_to_play_store.
  • Sending iOS builds to TestFlight with upload_to_testflight.

Protecting Sensitive Information

Security is a top priority. Git Secret encrypts API keys and credentials, keeping them safe even if the repo gets compromised.

Branching and Deployment

Branching and deployment follows a simple approach:

  • Feature branches build the custom CI image for consistency.
  • Tagged commits allow manual staging/production deployments — which manual job becomes available depends on the tag's shape: a vX.Y.Z-rc.N tag (e.g. v7.5.0-rc.1) exposes the staging deploy jobs, while a plain vX.Y.Z tag (e.g. v7.5.0) exposes the production ones. See Branching Strategy for the full release-candidate workflow.
  • Manual deployment jobs ensure only vetted versions reach users.

Environment Configuration

The pipeline maintains two distinct environments for stability:

  • Staging: For testing with .env.staging configs.
  • Production: Live environment using .env.production.

Staging Application Setup

The pipeline builds staging as an independently installable application rather than publishing staging builds under the production identifiers:

Platform Application ID Name Distribution
Android com.base.coldtivate.staging Coldtivate Staging Google Play internal testing
iOS com.base.coldtivate.staging Coldtivate Staging TestFlight

Release-candidate tags use the format vMAJOR.MINOR.PATCH-rc.NUMBER, where NUMBER is between 1 and 999. They retain MAJOR.MINOR.PATCH as the public application version and include the RC sequence in the store build number. For example, v7.5.0-rc.1 uses build 70500001. See Branching Strategy for the tagging workflow.

The GitLab protected-tag configuration must cover both release-candidate and regular release tags so protected signing variables are available to the manual deployment jobs.

Android Signing and Distribution

  1. Create the com.base.coldtivate.staging Play Console application and enable Play App Signing.
  2. Generate a dedicated upload keystore and store it outside the repository. The application expects the non-sensitive alias coldtivate-staging-upload.
  3. Grant the existing CI Google service account access to the staging application and its internal testing track.
  4. Configure these protected, masked GitLab CI/CD variables:
    • COLDTIVATE_STAGING_ANDROID_KEYSTORE_BASE64: the single-line base64-encoded upload keystore.
    • COLDTIVATE_STAGING_ANDROID_SIGNING_PASSWORD: the password used for the keystore and key.

Generate the upload key with:

keytool -genkeypair -v \
  -keystore coldtivate-staging-upload.jks \
  -alias coldtivate-staging-upload \
  -keyalg RSA \
  -keysize 2048 \
  -validity 10000

Encode it for GitLab without putting the password on the command line:

base64 < coldtivate-staging-upload.jks | tr -d '\n'

Back up the original keystore and password in the team's secure credential storage. Do not commit the keystore.

After the first Play upload, copy the Play app-signing certificate's SHA-256 fingerprint from App integrity into the com.base.coldtivate.staging entry in base-frontend/static/.well-known/assetlinks.json. Do not use the upload-key fingerprint. Deploy the association file to app-staging.coldtivate.org before verifying Android App Links.

iOS Signing and Distribution

  1. Register com.base.coldtivate.staging as an explicit Apple App ID and enable Associated Domains.
  2. Create the Coldtivate Staging application in App Store Connect for TestFlight distribution.
  3. Run Match once with write access to create the staging App Store provisioning profile:
bundle exec fastlane match appstore \
  --app_identifier com.base.coldtivate.staging \
  --readonly false

Confirm that Match encrypted and pushed the generated profile to the signing-assets repository. CI subsequently consumes the signing assets in read-only mode.

Add JS75VC2AMT.com.base.coldtivate.staging to the hosted Apple App Site Association file and deploy it to app-staging.coldtivate.org before verifying universal links.

Build Verification

The following commands build without uploading:

bundle exec fastlane android build pipeline:staging version_id:v0.0.1-rc.1
bundle exec fastlane ios build pipeline:staging version_id:v0.0.1-rc.1

The environment-specific android staging and ios staging lanes build and upload to Google Play and TestFlight respectively. Run those publishing lanes only when an upload is intended.

After building:

  • Confirm Android package IDs with adb shell pm list packages | grep coldtivate.
  • Inspect archived iOS entitlements with codesign -d --entitlements :- <app-path>.
  • Confirm staging and production can be installed together.
  • Confirm each application connects to its matching backend and opens only its own HTTPS domain and custom URL scheme.

Merge-request E2E Validation

Every merge-request pipeline builds a debug-signed Android E2E APK and runs the ordered Maestro suite against Docker services reset from the E2E database template. The test job is blocking and publishes JUnit results plus Maestro, Android, and backend diagnostic artifacts. See QA Automation for pipeline behavior and Mobile E2E Testing for local execution and troubleshooting.

Tracked environment files should contain placeholders only for secret values. Official mobile build jobs inject sensitive values such as the Sentry DSN at build time through GitLab CI/CD variables.

Toolchain Baseline

The mobile pipeline must use a current React Native-compatible toolchain:

  • Node.js: 22.11 or later
  • Yarn: 1.22.x
  • Java: 17
  • Xcode (iOS builds): 26 or later
  • iOS SDK (iOS builds): 26 or later
  • CocoaPods: able to resolve and install the iOS workspace dependencies for the current React Native version

For iOS, the selected Xcode on the runner must point to the Xcode 26 installation before the archive job runs.

Key Considerations

  • Secret Management: Keep those GPG keys safe and make sure secrets are encrypted properly.
  • Fastlane Configuration: Customize Fastlane lanes for the project's needs.
  • Caching: Speed up builds with GitLab's cache features.
  • Release Control: Manual triggers for major deployments keep things safe.
  • CI Image Maintenance: Update the CI image regularly with newer dependencies and tools. After the React Native 0.84 upgrade, stale Node/Xcode images are a likely source of broken builds.
  • Code Signing: Make sure iOS builds have proper code signing set up.
  • Google Service Account: Keep the service account json file in the right spot.

Validation After Toolchain Changes

When updating React Native or the Apple toolchain, validate all of the following before considering the pipeline stable again:

  • yarn install completes on the CI image
  • pod install completes on the macOS runner
  • Android debug and release builds succeed
  • iOS archive succeeds from the workspace using Xcode 26+

Mobile Secret Injection

For the React Native app, secrets required only for official builds should be injected in CI rather than committed to .env.* files.

Examples:

  • SENTRY_DSN_STAGING
  • SENTRY_DSN_PRODUCTION
  • SENTRY_AUTH_TOKEN

Forks and untrusted pipelines should not receive these variables.