App developers always strive to improve the product they are working on. So it is definite that either(mostly) you will be adding new features to existing products or (luckily) you will be setting up a whole new project. Either way, onboarding new developers or bootstrapping an entirely new project is a pretty challenging task. If not executed carefully, technical debt arises and headaches for developers, managers, sales people, and most importantly, clients.

Git-flow: A quick overview

Git flow is reasonably used in Agile as well as conventional Waterfall-based projects as well.

Before we start our thought process about implementing Continous Integration and Continous Delivery, it is crucial to decide your own git branching model. It should resemble your development workflow to make use of CI/CD tools.

Git branching model

This article is focused on mobile applications. Therefore, we will use the following git branching model for which serves the purpose as an example.

Figure 1: Git Branching Model by Vincent Driessen

Git flow is essentially a merge base solution. It does not rebase the branch. Also, keeping tag name format vX.Y.Z-beta-N helps to generate a changelog using automated utilities.

Neither Github-flow nor Git-flow in its purest form can help us adopt a resilient development workflow. The modern trend of Github usually seems to be having at least two branches, develop and master where develop branch is always under active development and master are holding the latest tagged releases at any given time.

Git-flow(with GitHub-flow tweaks) will help us come to a concrete solution for CI and CD. Of course, depending on your team sizes, you may have a different flow, but that flow should also be resembling the above-mentioned Git Branching Model.

Implementing Continous Integration & Delivery

By definition, an automated process or practice employed by software developers collaborating on a single software project of integrating code changes on the shared repository. This automation process may be composed of steps like testing code, packaging, and delivering and deployment to production.

During product development, developers have often been requested to give a new IPA file or APK file to the QA team or the client for checking and testing the features. Extending your workflow with CI/CD for unit-testing, distributing your binary can significantly reduce your off-development burden.

Advantages of CI/CD

CI/CD combined with git-flow gives many benefits such as the following:

  • Saves developer time by sending automated builds to testing teams
  • Removes probability of inconsistency in builds(mainly caused due to local caching) by the guaranteed pristine build
  • Takes team engagement to the next level by encouraging open communication and free information flow
  • Reduces developer dependency by promoting knowledge sharing amongst team members
  • Improves developer confidence when merging code
  • Helps eliminate a class of bugs that might occur due to manual handling

There could be de-merits of employing CI in a small team or individual developer adding overheads. However, for developers working on multiple projects with many people, CI is a wise investment with very high returns in terms of time and money.

Fastlane

Fastlane is recommended to implement CI/CD. Historically it became a part of Fabric in 2015, later acquired by Google in 2017. However, depending on the team skills, you may implement the solution on some scripting languages like Shell Script, Ruby, Python etc. The community of Fastlane is vibrant, and you will find a lot of open-source plugins for your business use case.

We have chosen Fastlane tools for implementing our CI/CD Solutions. It does require some knowledge of Ruby however, you don’t need to be an expert to work with it since it is implemented using Ruby-similar to widely used iOS dependency manager CocoaPods. (+1 to Gradle for its built-in dependency management function for Android)

Articles on how to get started with Fastlane can be found here. There is reasonably good documentation for both iOS and Android. However, we had difficulties finding some details related to Android Fastlane actions though we could search on forums like StackOverflow.

Modeling Git Branching Model to Fastlane

Usually, each major branches in our Git Branching Model correspond to an environment as shown in the table below:

Environment Alias/Tag Branch Description
Development alpha develop All the features for next or distant releases are usually developed in this environment
Staging beta release/ or hotfix/ Pre-release environment
Production prod master Build made on master always uses this environment

More complex flows may have additional layers of environment separation. For example, we can categorize our environment in the following as a lane (a.k.a Ruby function):

# Fastfile

ALIAS_MAP = {
  'alpha' => 'Develop',
  'beta' => 'Staging',
  'prod'=> 'Production' 
}
...
  desc 'Build alpha IPA'
  lane :alpha do |options|
    build_app # This will be replaced with custom `build_deploy` private lane later
  end

  desc 'Build beta IPA'
  lane :beta do |options|
    build_app # This will be replaced with custom `build_deploy` private lane later
  end

  desc 'Build production ipa'
  lane :prod do |options|
    build_app # This will be replaced with custom `build_deploy` private lane later
  end
...
Listing 1

ℹ️We may use lane options parameters to pass command line arguments from CI YAML file.

You can put some configuration of build_app action into Gymfile per lane basis as shown below:

# Gymfile
for_platform :ios do
    
    include_bitcode true
    include_symbols true

    for_lane :alpha do
      scheme '<YOUR_DEV_SCHEME>'
      export_method 'development' # or 'enterprise' for in-house testing
    end

    for_lane :beta do
      scheme '<YOUR_STAGING_SCHEME>'
      export_method 'ad-hoc'
    end

    for_lane :prod do
      scheme '<YOUR_PRODUCTION_SCHEME>'
      export_method 'app-store' # or 'enterprise' for release
    end
  end
Listing 2

For available configuration options for Gymfile follow the link here.

After putting above code from Listing 2 in a Gymfile, you don’t need to pass relavant settings like scheme and export_method into build_app action like below:

# You don't need to set parameters marked with 👈
# since it is handled in Gym file
build_app(
  scheme: "Release", # 👈 
  export_options: { # 👈 
    method: "app-store" # 👈 
  }
)
Listing 3

ℹ️Note: We will use xcconfig to set bundle id and provisioning profile unlike Listing 3

Firebase App Distribution

To distribute the built IPA binary to Firebase, Reading this document on how to setup Firebase App Distribution is highly recommended.

In Listing 1, we used build_app action to trigger the build. We can move that action into a private_lane called build_deploy and save duplicate code into these three main lanes. (Check Listing 7 at the end for further clarification)

  desc 'Build and deploy ipa'
  private_lane :build_deploy do |options|
    #1. Check if any new commits since last release
    is_releasable = analyze_commits(match: "*#{options[:environment]}*")
    unless is_releasable
      UI.important  "No changes since last one hence skipping build"
      next
    end
    
    #2. Increment build number
    increment_build_number(
      build_number:  lane_context[SharedValues::RELEASE_NEXT_VERSION] # set a specific number
    )
    #3. If you can use `match`, you use `match`.
    setup_provisioning_profiles
    
    #4. Build deploy
    build_app
    deploy_app options
  end
Listing 4
Step 1. Check if any new commits since the last release

analyze_commits is a third-party action for semantic release but quite helpful if you follow conventional commits. It lets us check if there has been any change since the last release. If there is then we go further otherwise stop with a message-“No changes since the last one hence skipping build”. This will help us save some build minutes on the CI machine.

Step 2. Increment build number

We can keep the Marketing Version and Internal Build Version separate. For example, if the Xcode project has AGV tooling enabled, we can use increment_build_number, which will automatically change the build number in the target.

Step 3. Setup Provisioning Profiles

We can use the Fastlane match command here. In case if we don’t have access, we may need to install it manually using import_certificate first and then performing FastlaneCore::ProvisioningProfile.install.

Step 4. Build and deploy

We use build_app action as we used in Listing 1. After the IPA file is ready we have to send it to Firebase App Distribution using deploy_app a private lane.

Deploying

We deploy to Firebase for alpha and beta only. For prod upload either we manually upload to App Store Connect or automate it using upload_to_testflight action. We will limit our discussion to upload prod IPA as an asset on Github Release only.

The deploy_app lane is also self-explanatory as shown in the listing below. We can divide the process in 5 steps:

  private_lane :deploy_app do |options|
    environment = options[:environment]
    next if environment == 'prod' # Since `prod` is uploaded to testflight and app store
    #1. Generate Change Log
    notes = conventional_changelog(title: "Change Log", format: "plain")
    
    #2. Get Google App ID
    gsp_path = "SupportingFiles/GoogleService/#{ALIAS_MAP[environment]}-Info.plist"
    google_app_id = get_info_plist_value(path: gsp_path, key: 'GOOGLE_APP_ID')
    
    #3. Upload to firebase
    firebase_app_distribution(
      app: google_app_id,
      # groups: tester_group,
      release_notes: notes,
      firebase_cli_path: FIREBASE_CLI_PATH
    )
    
    #4. Upload dSYM files to crashlytics
    upload_symbols_to_crashlytics(gsp_path: gsp_path)
    clean_build_artifacts
  end
Listing 5
Step 1. Generating Change Log:

We are using the semantic-version plug-in of Fastlane to generate these logs. conventional_changelog has to be used in conjunction with analyze_commits(which we have used in build_deploy lane to check is_releasable). analyze_commits takes match argument-a regex for matching with the previous git-tag like v1.0.1-beta-5. This helps to generate logs for only between the last tag and current v1.0.1-beta-6.

Step 2. Get Google App ID

We need google_app_id from the relevant GoogleService-Info.plist. This Plist is generated on Firebase. Our sample code project is a multi-configuration single target Xcode project. Three GoogleService plists are renamed and moved to GoogleService folder:

  • GoogleService/Develop-Info.plist
  • GoogleService/Staging-Info.plist
  • GoogleService/Production-Info.plist

We get gsp_path first and from that we get google_app_id.

Step 3. Upload to firebase

To use this plugin we have to install firebase CLI as well. On CircleCI within setup command we have installed npm package for firebase-cli. firebase_app_distribution is nothing but a wrapper to use the CLI to upload to Firebase. We need to give firebase_cli_path so that the appropriate binary is used.

Step 4. Upload dSYM files to crashlytics

Finally we upload dSYM files to Firebase. This will help Firebase make crash reports de-symbolized and readable.

Note: When bitcode is enabled in an Xcode project, App Store recompiles the code and provides us with dSYM files. These files need to be downloaded and uploaded to Firebase Crashlytics for crash report de-symbolization. Therefore, for the production version only we need to perform this step. checkout download_dsym action for Fastlane.

Github Release

release_on_github is a private lane and helps us automatically tag the commit, add release notes and attach IPA file for production, which can be later uploaded to App Store Connect.

Since Firebase doesn’t have APIs to download the IPA file except from installing it on device only-in case where you may want to give beta releases to the client using other distribution like deploygate-You may want to keep the IPA as a pre-release in Github release history.

Uploading IPA as an asset

  desc "Release on github"
  private_lane :release_on_github do |options|
    environment = options[:environment]
    #1. Generate Change Log
    notes = conventional_changelog(title: "Change Log")
    
    #2. Get Version and Build Number
    version_number = get_version_number
    build_number = get_build_number
    
    #3. Set Github Release
    is_prerelease = environment == 'beta' ? true : false

    name =  "[#{ALIAS_MAP[environment]}] v#{version_number} Build: #{build_number}}"

    set_github_release(
      repository_name: "#{ENV['CIRCLE_PROJECT_USERNAME']}/#{ENV['CIRCLE_PROJECT_REPONAME']}",
      name: name,
      commitish: ENV['CIRCLE_SHA1'],
      description: notes,
      tag_name: "v#{version_number}-#{options[:environment]}-#{build_number}",
      is_prerelease: is_prerelease,
      upload_assets: [lane_context[SharedValues::IPA_OUTPUT_PATH]]
    )
  end
Listing 6
Step 1. Generate Change Log

conventional_changelog generates by default mark-down style notes. This is handy when preparing auto-release notes.

Step 2. Get Version and Build Number

We use these to set the title of the release. AGV tooling should be enabled in the XCode project to use get_version_number and get_build_number.

Step 3. Set Github Release

We check if its beta then marks it as Pre-Release. Based on the environment we format the name/ title for the release notes. Set the tag name in a format like v1.0.1-beta-1234 and upload the built IPA file as an asset to the release.

Note: You will need to set your personal token or ci bot token to GITHUB_API_TOKEN in the environment. This token should have permission to create a tag.

Revising Main Lanes in Listing 1

We will skip the alpha releases on Github because they are quite frequent in releases and stored on the Firebase. Hence, you can save some space on Github by not tagging them and causing it hard to navigate release history on Github.

# Fastfile

ALIAS_MAP = {
  'alpha' => 'Develop',
  'beta' => 'Staging',
  'prod'=> 'Production' 
}
...
  desc 'Build alpha IPA'
  lane :alpha do |options|
    build_deploy options
    # Not releasing to Github since Firebase App Distribution
  end

  desc 'Build beta ipa'
  lane :beta do |options|
    build_deploy options
    release_on_github options
  end

  desc 'Build production ipa'
  lane :prod do |options|
    build_deploy options
    release_on_github options
  end
...
Listing 7

Matching up CircleCI configuration with Fastlane

CircleCI uses YAML files which should be simple. When a pull request is merged or code is pushed to some branch, it may trigger a CircleCI workflow given a properly set config.yml which in turn fires a specific lane on Fastlane.

Please carefully note that we have three environment arguments called alpha, beta and prod which you can refer in Table 2 in the alias column. Have a look at snippet of yaml file below:

...
jobs:
 deploy:
    executor:
      name: default
    parameters:
      build_type:
        type: enum
        enum: ['alpha','beta', 'prod'] # Corresponds to lanes
        default: alpha
    steps:
      - attach_workspace:
          at: /Users/distiller/project
      - run:
          name: Build ipa
          command: bundle exec fastlane ios << parameters.build_type >>
      - store_artifacts:
          path: output
          when: on_success
...

Following is also snippet of the same yaml file. Here setup installs required dependency like rubygems, npm modules, cocoapod, firebase cli, carthage etc.

...
workflows:
  main:
    jobs:
      - setup
      - test:
          requires:
            - setup
      - deploy:
          name: build_deploy_alpha
          build_type: alpha
          requires:
            - setup
          filters:
            branches:
              only:
                - develop # RegEx
      - deploy:
          name: build_deploy_beta
          build_type: beta
          requires:
            - setup
          filters:
            branches:
              only:
                - /release\/.*/ # RegEx
                - /hotfix\/.*/ # RegEx
      - deploy:
          name: build_deploy_prod
          build_type: prod
          requires:
            - setup
          filters:
            branches:
              only:
                - master # RegEx

...

Pay special attention to the filters of the each workflow as well. They are containing Regular Expressions like develop, /release\/.*/, /hotfix\/.*/,master.

These filters make sure that CI build is triggered only for those branches which match above regex.alpha builds are triggered when code is pushed to develop, beta for release/* and prod for master.

Push On Branch Lane executed Environment
develop bundle exec fastlane ios alpha Development
release/* & hotfix/* bundle exec fastlane ios beta Staging
master bundle exec fastlane ios prod Production

The only purpose of showing the above snippet is to show you how the git branching model is applied to the CircleCI YAML file configuration and the Fastlane configuration file. You can learn more about CircleCI Yaml here.

Conclusion

Effective development workflow and CI-CD implementation can help reduce tons of developer hours. Similarly, the QA team will be able to link the bugs and issues to a particular build and can have a more productive conversation with developers. Especially, ramping up time for the new developer also reduces significantly.

Resources

Sample Code Fastlane Tools Docs Firebase App Distribution Git Branching Model by Vincent Driessen AGV tooling enabled

Article Photo by Rasa Kasparaviciene

Author

Aarif Sumra

Head of iOS/Tech Lead

love to learn and share everything iOS

You may also like

RubyKaigi Takeout 2021: A Look Back

Can you believe it’s already been an entire year since I had the opportunity to join my first ever developers conference, the RubyKaigi? Over the past three days, I was blessed with the opportunity to also enjoy this year’s talks and sessions, and get once again the opportunity to immerse...

AFK
Safe Navigation With Jetpack Compose

Navigation is the key part of application development and Android is no exception. From Activities and Fragments transitions to Navigation Component and now, Navigation Component is available for Jetpack Compose! In this article, I would like to give a brief overview of how Jetpack Compose Navigation works, the problems I’ve...