CI/CD with GitHub Actions for Mobile Apps

Table of Contents
Big thanks to our contributors those make our blogs possible.

Our growing community of contributors bring their unique insights from around the world to power our blog. 

Introduction

Automating build, test, and deployment pipelines is essential for modern mobile teams to release high‑quality apps with speed and reliability. GitHub Actions provides a native, declarative CI/CD solution directly within your repo—enabling you to:

  • Build on macOS (iOS) and Ubuntu (Android) runners
  • Test unit, UI, and integration scenarios across simulator/emulator matrices
  • Sign code securely using encrypted certs, profiles, and keystores
  • Cache dependencies (CocoaPods, Gradle, npm) for fast iterative runs
  • Deploy to TestFlight, App Store, Google Play, or internal distribution channels
  • Notify teams of build status via Slack, email, or other integrations

This guide walks you through designing a comprehensive mobile CI/CD pipeline in GitHub Actions—covering workflow structure, runner selection, key stages, secure signing, artifact management, deployment automation, and best practices for maintainable, modular workflows.

Table of Contents

  1. Pipeline Stages Overview
  2. Runner Selection & Matrices
  3. Android Workflow Example
  4. iOS Workflow Example
  5. Secure Code Signing
  6. Deployment Automation
  7. Notifications & Monitoring
  8. Best Practices & Modularization
  9. Folder Structure & Reusable Actions
  10. Conclusion

Pipeline Stages Overview

Every mobile CI/CD pipeline typically follows these stages:

StagePurpose
Checkout & SetupFetch code, set up runtime (JDK, Ruby, Node, SDKs)
Dependency InstallationInstall CocoaPods, Gradle dependencies, npm modules
Build & CompileProduce debug/release APKs, IPAs
Static Analysis & LintSwiftLint, ESLint, detekt, etc.
Unit & UI TestsRun tests on simulators/emulators, real devices if available
Code Signing & ProvisioningDecrypt and install profiles, keystores, certificates
Artifact ArchivalUpload build artifacts for distribution or debugging
DeploymentFastlane pilot/supply, custom scripts to stores or internal

Runner Selection & Matrices

PlatformRunnerUse Cases
iOSmacos-latestXcode build, CocoaPods, SwiftLint
Androidubuntu-latestGradle build, Android emulator
All Platformsself-hosted macOSCustom Xcode versions, private devices

Matrix Builds allow parallelizing across versions:

yamlCopyEditstrategy:
  matrix:
    api-level: [29, 30, 31]
    jdk: [11, 17]

Android Workflow Example

yamlCopyEditname: Android CI/CD

on:
  push:
    branches: [ main ]
  pull_request:

jobs:
  build-test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        jdk: [ '11', '17' ]
        api-level: [ 29, 30, 31 ]

    steps:
      - name: Checkout code
        uses: actions/checkout@v3

      - name: Setup JDK ${{ matrix.jdk }}
        uses: actions/setup-java@v3
        with:
          distribution: 'temurin'
          java-version: ${{ matrix.jdk }}

      - name: Cache Gradle
        uses: actions/cache@v3
        with:
          path: |
            ~/.gradle/caches
            ~/.gradle/wrapper
          key: ${{ runner.os }}-gradle-${{ matrix.jdk }}-${{ matrix.api-level }}-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}

      - name: Accept Android SDK Licenses
        run: yes | sdkmanager --licenses

      - name: Build Debug APK
        run: ./gradlew assembleDebug

      - name: Run Unit Tests
        run: ./gradlew testDebugUnitTest

      - name: Run Instrumented Tests
        run: |
          # Create and start emulator
          echo "no" | avdmanager create avd -n test -k "system-images;android-${{ matrix.api-level }};google_apis;x86_64"
          emulator -avd test -no-window -no-audio &
          adb wait-for-device
          ./gradlew connectedAndroidTest

      - name: Archive Debug APK
        uses: actions/upload-artifact@v3
        with:
          name: app-debug.apk
          path: app/build/outputs/apk/debug/app-debug.apk

Key Points:

  • Matrix for JDK and API levels ensures broad coverage
  • Gradle Cache reduces build time significantly
  • Emulator Setup headless for instrumented tests

iOS Workflow Example

yamlCopyEditname: iOS CI/CD

on:
  push:
    branches: [ main ]
  pull_request:

jobs:
  build-test:
    runs-on: macos-latest

    steps:
      - name: Checkout Code
        uses: actions/checkout@v3

      - name: Setup Ruby & Bundler
        uses: ruby/setup-ruby@v1
        with:
          ruby-version: '3.0'
          bundler-cache: true

      - name: Install Dependencies
        run: |
          cd ios
          bundle install
          bundle exec pod install

      - name: Cache CocoaPods
        uses: actions/cache@v3
        with:
          path: ios/Pods
          key: ${{ runner.os }}-pods-${{ hashFiles('ios/Podfile.lock') }}

      - name: Run SwiftLint
        run: bundle exec swiftlint lint

      - name: Build & Test on Simulator
        run: |
          xcodebuild clean test \
            -workspace ios/MyApp.xcworkspace \
            -scheme MyApp \
            -destination 'platform=iOS Simulator,name=iPhone 14,OS=16.0' \
            -enableCodeCoverage YES

      - name: Archive Development IPA
        run: |
          bundle exec fastlane gym \
            workspace:"ios/MyApp.xcworkspace" \
            scheme:"MyApp" \
            export_method:"development" \
            output_directory:"build/ios" \
            output_name:"MyApp.ipa"

      - name: Upload IPA
        uses: actions/upload-artifact@v3
        with:
          name: MyApp.ipa
          path: build/ios/MyApp.ipa

Highlights:

  • Bundler Cache speeds up gem installs
  • CocoaPods Cache saves time on pod install
  • SwiftLint enforces style before build
  • Fastlane gym for consistent IPA packaging

Secure Code Signing

PlatformTool/ActionDescription
iOSfastlane match / OpenSSLCentralized profiles via Git or encrypted p12 import
AndroidGPG + Gradle propertiesDecrypt keystore.jks.gpg and pass signing props

iOS: Decrypt & Install

yamlCopyEdit- name: Decrypt iOS Certificates
  run: |
    echo $MATCH_PASSWORD | fastlane match appstore --readonly

or manual:

yamlCopyEdit- name: Decrypt p12 & profiles
  run: |
    openssl aes-256-cbc -K ${{ secrets.KEY }} -iv ${{ secrets.IV }} \
      -in ios/certs/distribution.p12.enc -out dist.p12 -d
    security import dist.p12 -k ~/Library/Keychains/login.keychain -P ""
    mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
    cp ios/certs/profile.mobileprovision ~/Library/MobileDevice/Provisioning\ Profiles/

Android: Decrypt Keystore

yamlCopyEdit- name: Decrypt Android Keystore
  run: |
    echo $DECRYPT_PASSWORD | gpg --batch --yes --passphrase-fd 0 \
      -o android/keystore.jks -d android/myapp.keystore.jks.gpg

Use android.injected.signing.* Gradle properties in your build step.

Deployment Automation

Android: Play Store via Fastlane supply

yamlCopyEdit- name: Deploy to Play Store (Internal)
  run: fastlane supply \
    track: internal \
    json_key_data: ${{ secrets.PLAY_STORE_JSON_KEY }} \
    apk: app/build/outputs/apk/release/app-release.apk

iOS: TestFlight via Fastlane pilot

yamlCopyEdit- name: Upload to TestFlight
  run: fastlane pilot upload \
    ipa:build/ios/MyApp.ipa \
    skip_waiting_for_build_processing:true

Manual Approvals: Use workflow_dispatch and require GitHub environment approvals for production lanes.

Notifications & Monitoring

Integrate Slack, Teams, or email notifications for pipeline status:

yamlCopyEdit- name: Notify Slack
  uses: 8398a7/action-slack@v3
  with:
    status: ${{ job.status }}
    fields: repo,commit,author
  env:
    SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}

Monitor build times, flake rates, and deployment health via actionable dashboard or GitHub checks.

Best Practices & Modularization

  • Split Workflows: Separate Android and iOS into distinct YAML files for clarity.
  • Composite Actions: Encapsulate common steps (checkout, setup, cache) for reuse across multiple repos.
  • Environment Variables: Centralize all secrets in GitHub Secrets, prefixing by platform (IOS_, ANDROID_).
  • Fail Fast: Run lint and unit tests before lengthy emulator/simulator or signing steps.
  • Version Control Artifacts: Upload IPA/APK and logs as workflow artifacts for diagnostics.
  • Documentation: Maintain a CI.md explaining workflow setup, secret requirements, and how to run locally via act.

Folder Structure & Reusable Actions

arduinoCopyEdit.github/
└── workflows/
    ├── android-ci.yml
    ├── ios-ci.yml
    └── deploy.yml

.github/actions/
├── setup-checkout/
│   └── action.yml     # Checkout + repo setup
└── cache-setup/
    └── action.yml     # CocoaPods/Gradle cache logic

fastlane/
├── ios/
│   ├── Fastfile
│   └── Matchfile
└── android/
    └── Fastfile
  • Custom Actions let you share logic (cache keys, emulator setup) across projects.
  • Deploy Workflow can be manually triggered (workflow_dispatch) and gated by environment approvals.

Conclusion

By leveraging GitHub Actions and Fastlane, you can build a robust, end‑to‑end CI/CD pipeline for both iOS and Android:

  1. Automated Builds across simulator/emulator matrices
  2. Static Analysis and Unit/UI Tests early in the pipeline
  3. Secure Code Signing with match/GPG decryption
  4. Artifact Archival for debugs and audits
  5. Automated Deployment to TestFlight and Play Store
  6. Notifications & Approvals to keep stakeholders informed

Start by crafting simple build-test workflows, then incrementally add signing, deployment, and notifications. With modular, reusable actions and rigorous secret management, your mobile teams will ship features faster, catch regressions early, and maintain confidence in every release—across platforms, on every device.

Let's connect on TikTok

Join our newsletter to stay updated

Sydney Based Software Solutions Professional who is crafting exceptional systems and applications to solve a diverse range of problems for the past 10 years.

Share the Post

Related Posts