IOS CI CD Pipeline with GitHub Actions and Fastlane - dhruvin207/android-common-utils GitHub Wiki

This document provides a comprehensive guide on setting up an automated CI/CD pipeline for an iOS application using GitHub Actions and Fastlane. The pipeline will build your app and deploy it to Firebase App Distribution, ensuring continuous integration and delivery for seamless app updates distribution.

🚀 Benefits of this Automation

  • Streamlined CI/CD Process: Automates the build and deployment process, reducing manual effort and human error.
  • Certificate Management: Automatically manages iOS certificates and profiles using Fastlane Match, ensuring they're always up-to-date.
  • Secure Distribution: Uses Firebase App Distribution to securely distribute builds to testers.
  • Scalable: Can be extended to deploy to TestFlight or App Store with minimal changes.

🔐 Prerequisites

  1. Apple Developer Account:

    • Ensure you have an Apple Developer account with appropriate permissions.
    • The app should be created in the Apple Developer Console.
    • You will use this account to sync certificates and profiles automatically with Fastlane.
  2. GitHub Repository:

    • A GitHub repository (public or private) to store the certificates and profiles.
    • You need a GitHub token with access to this repo for Fastlane to fetch and manage certificates.
    • Encrypt and decrypt certificates using a passphrase for security.
  3. Firebase Project:

    • Create a Firebase project for your app and enable App Distribution.
    • Obtain the Firebase App ID and Tester Group name from your Firebase project.
  4. Google Cloud Service Account:

    • Create a Service Account in Google Cloud with Firebase App Distribution Admin role.
    • Generate a JSON key for this account which will be used to authenticate Fastlane with Firebase.

⚙️ Setting Up the Automation

Step 1: GitHub Action Workflow Setup

Below is a step-by-step breakdown of each part of the GitHub Actions workflow configuration.

1. Trigger on Push

on:
  push:
    branches:
      - dev  
  • Explanation: This configuration triggers the workflow whenever there is a push to the dev branch. It ensures that every change pushed to dev will automatically go through the CI/CD pipeline.

2. Define the Job

jobs:
  ios-build-and-deploy:
    runs-on: macOS
    timeout-minutes: 30
  • Explanation: The job named ios-build-and-deploy runs on a macOS machine, which is necessary for building iOS apps. The timeout-minutes is set to 30 minutes to limit the execution time, preventing indefinite hangs.

3. Checkout Repository

- name: 📂 Checkout repository
  uses: actions/checkout@v4
  with:
    fetch-depth: 0
  • Explanation: This step checks out the code from the GitHub repository. The fetch-depth: 0 option ensures the entire history is fetched, which might be required for certain CI processes.

4. Install Fastlane

- name: ⬆️ Install Fastlane
  run: |
    bundle install
    brew install fastlane
  • Explanation: Installs Fastlane on the GitHub-hosted macOS runner. First, dependencies are installed using Bundler (bundle install), and then Fastlane is installed using Homebrew (brew install fastlane).

5. Install Firebase App Distribution Plugin

- name: 🔌 Install Firebase app distribution plugin
  run: |
    bundle exec fastlane add_plugin firebase_app_distribution
  • Explanation: This step adds the Firebase App Distribution plugin to Fastlane. The plugin is essential for deploying the built app to Firebase.

6. Install Dependencies

- name: 📦 Install dependencies
  run: |
    Pod Install
  • Explanation: Installs CocoaPods dependencies required for the iOS project. This is necessary for building the app correctly.

7. Set Environment Variables

- name: 🛠️ Set environment variables
  run: |
    echo "FIREBASE_APP_TESTER_GROUP=${{ secrets.FIREBASE_APP_TESTER_GROUP }}" >> "$GITHUB_ENV"
    echo "FIREBASE_IOS_APP_ID=${{ secrets.FIREBASE_IOS_APP_ID }}" >> "$GITHUB_ENV"
    echo "IOS_FASTLANE_APPLE_ID_PASSWORD=${{ secrets.IOS_FASTLANE_APPLE_ID_PASSWORD }}" >> "$GITHUB_ENV"
    echo "IOS_MATCH_KEYCHAIN_PASSWORD=${{ secrets.IOS_MATCH_KEYCHAIN_PASSWORD }}" >> "$GITHUB_ENV"
    echo "IOS_MATCH_PASSWORD=${{ secrets.IOS_MATCH_PASSWORD }}" >> "$GITHUB_ENV"
    echo "IOS_MATCH_GIT_BASIC_AUTHORIZATION_BASE64=${{ secrets.IOS_MATCH_GIT_BASIC_AUTHORIZATION_BASE64 }}" >> "$GITHUB_ENV"
    echo "IOS_MATCH_USERNAME=${{ vars.IOS_MATCH_USERNAME }}" >> "$GITHUB_ENV"
    echo "IOS_MATCH_APPLE_TEAM_ID=${{ vars.IOS_MATCH_APPLE_TEAM_ID }}" >> "$GITHUB_ENV"
    echo "IOS_MATCH_GIT_REPO_URL=${{ vars.IOS_MATCH_GIT_REPO_URL }}" >> "$GITHUB_ENV"
    
    printf '%s' "${{ secrets.SERVICE_CREDENTIALS_CONTENT_BASE64 }}" | base64 --decode > ${{ github.workspace }}/service_cred.json
    echo "SERVICE_CREDENTIALS_FILE=${{ github.workspace }}/service_cred.json" >> "$GITHUB_ENV"
  • Explanation: This step sets up the environment variables needed by Fastlane. These variables include credentials and other configuration values stored in GitHub Secrets and Variables. It also decodes and stores the Firebase service credentials JSON file required for deployment.

8. Build App

- name: 🏗️ Build App
  run: |
    bundle exec fastlane build
  • Explanation: This step triggers the Fastlane build lane, which will handle syncing certificates, installing pods, and building the iOS app.

9. Deploy App

- name: 🚀 Deploy App
  run: |
    bundle exec fastlane deploy
  • Explanation: This step triggers the Fastlane deploy lane, which will upload the built IPA file to Firebase App Distribution.

Step 2: Fastlane Setup in the Project

After configuring the GitHub Actions workflow, the next step is to set up Fastlane within your project.

1. Install Fastlane

brew install fastlane
  • Explanation: Installs Fastlane on your local machine using Homebrew.

2. Initialize Fastlane in Your Project

fastlane init
  • Explanation: Initializes Fastlane in your project, creating a Fastfile and other necessary configuration files.

Step 3: Fastlane Configuration

The following is the Fastlane configuration file (Fastfile) that defines the lanes for building and deploying your iOS app.

1. Default Platform Setup

default_platform(:ios)
  • Explanation: Specifies that the default platform for this Fastlane configuration is iOS.

2. Build Lane

platform :ios do
  
  desc "Sync Certificates and Build the iOS IPA for firebase distribution"
  lane :build do
      # Set these Env from github Env to run the fastlane 'Match' action.
      ENV['FASTLANE_PASSWORD'] = ENV['IOS_FASTLANE_APPLE_ID_PASSWORD']
      ENV['MATCH_KEYCHAIN_PASSWORD'] = ENV['IOS_MATCH_KEYCHAIN_PASSWORD']
      ENV['MATCH_PASSWORD'] = ENV['IOS_MATCH_PASSWORD']
      ENV['MATCH_GIT_BASIC_AUTHORIZATION'] = ENV['IOS_MATCH_GIT_BASIC_AUTHORIZATION_BASE64']
    
      xcode_select("/Applications/Xcode.app")
      install_pod()
      sync_certificates()
      build_ipa()
  end
  • Explanation: The build lane is responsible for syncing certificates, installing pods, and building the iOS app into an IPA file ready for distribution.

3. Install Pods Lane

  desc "Install Pods"
  private_lane :install_pod do
    cocoapods(
      use_bundle_exec: false
    )
  end
  • Explanation: A private lane that installs CocoaPods dependencies. This is called within the build lane.

4. Sync Certificates Lane

  desc "Sync certificate and profile"
  lane :sync_certificates do
       match(
          readonly: false, #read-only disables match from overriding the existing certificates.
          git_url: ENV['IOS_MATCH_GIT_REPO_URL'],
          git_branch: "main",
          type: "adhoc",
          app_identifier: "com.app.demo",
          username: ENV['IOS_MATCH_USERNAME'],
          team_id: ENV['IOS_MATCH_APPLE_TEAM_ID'],
          storage_mode: "git"
       )
  end
  • Explanation: This lane uses Fastlane Match to sync certificates and profiles with the GitHub repository. It fetches or generates the required certificates.

5. Build IPA Lane

  desc "Build the IPA"
  private_lane:build_ipa do
          gym(
            workspace: "App.xcworkspace",
            configuration:

 "Release",
            scheme: "App",
            clean: true,
            export_method: "ad-hoc",
            output_name: "app.ipa",
            output_directory:"build"
          )

          build_path = lane_context[SharedValues::IPA_OUTPUT_PATH]
          UI.important("Generated build path => #{build_path}")
          # Set the APK path as an environment variable for GitHub Actions
          sh("echo chmod +x #{build_path}")
          sh("echo BUILD_PATH=#{build_path} >> $GITHUB_ENV")
  end
  • Explanation: This private lane builds the IPA file using Fastlane Gym. It specifies the workspace, configuration, scheme, and other build parameters. The generated IPA path is then stored as an environment variable.

6. Deploy Lane

  desc "Deploy the IPA file to firebase"
  lane :deploy do
       service_cred_file_path = ((ENV["SERVICE_CREDENTIALS_FILE"]!="") ? ENV["SERVICE_CREDENTIALS_FILE"] : "")
       firebase_app_id = ((ENV["FIREBASE_IOS_APP_ID"]!="") ? ENV["FIREBASE_IOS_APP_ID"] : "")
       firebase_app_tester_group = ((ENV["FIREBASE_APP_TESTER_GROUP"]!="") ? ENV["FIREBASE_APP_TESTER_GROUP"] : "")
       release_notes=File.read('../../release_note.txt')
       
       if service_cred_file_path.nil? || service_cred_file_path.empty?
         UI.error("service_cred_file_path file is not accessible")
       end
  
       UI.header("Validate the google account credentials.....")
       validate_play_store_json_key(
         json_key: service_cred_file_path
       )
       
       firebase_app_distribution(
         debug: true,
         app: firebase_app_id,
         groups: firebase_app_tester_group,
         release_notes: release_notes,
         service_credentials_file: service_cred_file_path,
         ipa_path: ENV["BUILD_PATH"],
         googleservice_info_plist_path: "app/GoogleService-info.plist"
       )
  end
end
  • Explanation: The deploy lane deploys the IPA file to Firebase App Distribution. It uses environment variables for configuration and uploads the IPA using the Firebase App Distribution plugin.

📄 Full GitHub Actions Workflow File

name: IOS CI/CD

on:
  push:
    branches:
      - dev  
jobs:
  ios-build-and-deploy:
    runs-on: macOS
    timeout-minutes: 30
    steps:
      - name: 📂 Checkout repository
        uses: actions/checkout@v4
        with:
          fetch-depth: 0
             
      - name: ⬆️ Install Fastlane
        run: |
          bundle install
          brew install fastlane
 
      - name: 🔌 Install Firebase app distribution plugin
        run: |
          bundle exec fastlane add_plugin firebase_app_distribution
    
      - name: 📦 Install dependencies
        run: |
          Pod Install

      - name: 🛠️ Set environment variables
        run: |
          echo "FIREBASE_APP_TESTER_GROUP=${{ secrets.FIREBASE_APP_TESTER_GROUP }}" >> "$GITHUB_ENV"
          echo "FIREBASE_IOS_APP_ID=${{ secrets.FIREBASE_IOS_APP_ID }}" >> "$GITHUB_ENV"
          echo "IOS_FASTLANE_APPLE_ID_PASSWORD=${{ secrets.IOS_FASTLANE_APPLE_ID_PASSWORD }}" >> "$GITHUB_ENV"
          echo "IOS_MATCH_KEYCHAIN_PASSWORD=${{ secrets.IOS_MATCH_KEYCHAIN_PASSWORD }}" >> "$GITHUB_ENV"
          echo "IOS_MATCH_PASSWORD=${{ secrets.IOS_MATCH_PASSWORD }}" >> "$GITHUB_ENV"
          echo "IOS_MATCH_GIT_BASIC_AUTHORIZATION_BASE64=${{ secrets.IOS_MATCH_GIT_BASIC_AUTHORIZATION_BASE64 }}" >> "$GITHUB_ENV"
          echo "IOS_MATCH_USERNAME=${{ vars.IOS_MATCH_USERNAME }}" >> "$GITHUB_ENV"
          echo "IOS_MATCH_APPLE_TEAM_ID=${{ vars.IOS_MATCH_APPLE_TEAM_ID }}" >> "$GITHUB_ENV"
          echo "IOS_MATCH_GIT_REPO_URL=${{ vars.IOS_MATCH_GIT_REPO_URL }}" >> "$GITHUB_ENV"
          
          printf '%s' "${{ secrets.SERVICE_CREDENTIALS_CONTENT_BASE64 }}" | base64 --decode > ${{ github.workspace }}/service_cred.json
          echo "SERVICE_CREDENTIALS_FILE=${{ github.workspace }}/service_cred.json" >> "$GITHUB_ENV"

      - name: 🏗️ Build App
        run: |
          bundle exec fastlane build

      - name: 🚀 Deploy App
        run: |
          bundle exec fastlane deploy

📄 Full Fastlane File

# This file contains the fastlane.tools configuration
# You can find the documentation at https://docs.fastlane.tools
#
# For a list of all available actions, check out
#
#     https://docs.fastlane.tools/actions
#
# For a list of all available plugins, check out
#
#     https://docs.fastlane.tools/plugins/available-plugins
#

# Uncomment the line if you want fastlane to automatically update itself
# update_fastlane


default_platform(:ios)

platform :ios do
  
  desc "Sync Certificates and Build the iOS IPA for firebase distribution"
  lane :build do
      # Set these Env from github Env to run the fastlane 'Match' action.
      ENV['FASTLANE_PASSWORD'] = ENV['IOS_FASTLANE_APPLE_ID_PASSWORD']
      ENV['MATCH_KEYCHAIN_PASSWORD'] = ENV['IOS_MATCH_KEYCHAIN_PASSWORD']
      ENV['MATCH_PASSWORD'] = ENV['IOS_MATCH_PASSWORD']
      ENV['MATCH_GIT_BASIC_AUTHORIZATION'] = ENV['IOS_MATCH_GIT_BASIC_AUTHORIZATION_BASE64']
    
      xcode_select("/Applications/Xcode.app")
      install_pod()
      sync_certificates()
      build_ipa()
  end
  
  desc "Install Pods"
  private_lane :install_pod do
    cocoapods(
      use_bundle_exec: false
    )
  end
  
  desc "Sync certificate and profile"
  lane :sync_certificates do
       match(
          readonly: false, #read-only disables match from overriding the existing certificates.
          git_url: ENV['IOS_MATCH_GIT_REPO_URL'],
          git_branch: "main",
          type: "adhoc",
          app_identifier: "com.app.demo",
          username: ENV['IOS_MATCH_USERNAME'],
          team_id: ENV['IOS_MATCH_APPLE_TEAM_ID'],
          storage_mode: "git"
       )
  end
  
  desc "Build the IPA"
  private_lane:build_ipa do
          gym(
            workspace: "App.xcworkspace",
            configuration: "Release",
            scheme: "App",
            clean: true,
            export_method: "ad-hoc",
            output_name: "app.ipa",
            output_directory:"build"
          )

          build_path = lane_context[SharedValues::IPA_OUTPUT_PATH]
          UI.important("Generated build path => #{build_path}")
          # Set the APK path as an environment variable for GitHub Actions
          sh("echo chmod +x #{build_path}")
          sh("echo BUILD_PATH=#{build_path} >> $GITHUB_ENV")
  end
  

  desc "Deploy the IPA file to firebase"
  lane :deploy do
       service_cred_file_path = ((ENV["SERVICE_CREDENTIALS_FILE"]!="") ? ENV["SERVICE_CREDENTIALS_FILE"] : "")
       firebase_app_id = ((ENV["FIREBASE_IOS_APP_ID"]!="") ? ENV["FIREBASE_IOS_APP_ID"] : "")
       firebase_app_tester_group = ((ENV["FIREBASE_APP_TESTER_GROUP"]!="") ? ENV["FIREBASE_APP_TESTER_GROUP"] : "")
       release_notes=File.read('../../release_note.txt')
       
       if service_cred_file_path.nil? || service_cred_file_path empty?
         UI.error("service_cred_file_path file is not accessible")
       end
  
       UI.header("Validate the google account credentials.....")
       validate_play_store_json_key(
         json_key: service_cred_file_path
       )
       
       firebase_app_distribution(
         debug: true,
         app: firebase_app_id,
         groups: firebase_app_tester_group,
         release_notes: release_notes,
         service_credentials_file: service_cred_file_path,
         ipa_path: ENV["BUILD_PATH"],
         googleservice_info_plist_path: "app/GoogleService-info.plist"
       )
  end

end

🎯 Conclusion

This CI/CD pipeline automates the build and deployment process for your iOS application, ensuring that your app is continuously integrated and ready for distribution to testers through Firebase. This setup is scalable and can be extended to support TestFlight and App Store deployments.

🔧 Key Customizations

  • Environment Variables: Customize the environment variables in the workflow file according to your project's configuration.
  • Firebase Plugin: Ensure that the Firebase App Distribution plugin is correctly installed and configured.
  • Fastlane Lanes: Modify the lanes in Fastlane to fit your specific build and deployment requirements.