Opinion

CI/CD Template for Mobile Applications

Published on 15 May, 2023 by William

OVERVIEW

The main concepts of CI/CD are continuous integration, continuous delivery and continuous deployment.

CI/CD introduces automation to the lifecycle of a mobile application from development and testing through to delivery to an App Store. This automation process can be called a CI/CD pipeline.

The following is a platform-agnostic template for deploying an iOS and Android app to the relevant stores. The iOS app will be deployed to iOS TestFlight. The Android app will be deployed to Android Internal Test Track.

Promoting to production would be the final step after successful testing.

Repo Branch Structure

The branch structure should allow for a formal and structured approach to support

  • development, develop
  • quality assurance, qa
  • deployment to production, production

Code push and merge to a repository

Pipeline yaml file

The following is an example of a generic ‘.yml’ file for a CI/CD pipeline configuration. There are 2 workflows shown:

  • test
    • triggered every time a pull request (PR) is created.
    • performs ci test
  • build-ios-android-on-merge-into-qa
    • triggered on push into /qa branch

definitions: # instance types instance_mac_pro: &instance_mac_pro instance_type: mac_pro max_build_duration: 90 instance_linux: &instance_linux instance_type: linux max_build_duration: 60 . . . scripts: - &bundler_install_ios # bundle install will use the gem file to install fastlane. See Setup section below. script: | cd ios bundle install - &fastlane_ios_beta # Get secrets (keys) from AWS SecretsManager. This is not covered as part of this template. name: Build Sign Upload - iOS beta script: | # Build iOS and Sign Android app bundle exec fastlane ios beta --env $2 key_id:$3 issuer_id:$4 key_content:$5 s3_region:$6 s3_access_key:$7 s3_secret_access_key:$8 s3_bucket:$9 slack_url:${10} . . . workflows: test: # Test # - triggered via a pull request into any branch name: Test <<: *instance_linux environment: <<: *environment_vars_groups_both <<: *environment_versions_android triggering: events: - pull_request branch_patterns: - pattern: '*' include: true source: true cancel_previous_builds: false scripts: - echo 'test-npm-ci' - *npm_install_bootstrap - *npm_run_ci publishing: <<: *slack_publish build-ios-android-on-merge-into-qa: # Build iOS and Sign Android app # - this will put the build (.ipa) into TestFlight for client # - this will put the build (.aab) into the beta track in the Android Console for client name: Build iOS and Android on merge into QA <<: *instance_mac_pro working_directory: app environment: <<: *environment_vars_groups_both <<: *environment_versions_both triggering: events: - push branch_patterns: - pattern: qa include: true source: true cancel_previous_builds: false scripts: - *install_bootstrap - *bundler_install_ios - *bundler_install_android - *fastlane_android_beta - *fastlane_ios_beta artifacts: - *android_apk - *ios_ipa publishing: <<: *slack_publish

From the Yaml file, we can see that a PR merged into the qa branch will trigger build-ios-android-on-merge-into-qa. This will run the install, test and deployment scripts eg fastlane_ios_beta.

fastlane_ios_beta will call a Fastlane lane called beta. See Using Fastlane to build and sign the apps.

Using Fastlane to build and sign the apps

“fastlane is the easiest way to automate beta deployments and releases for your iOS and Android apps. It handles all tedious tasks, like generating screenshots, dealing with code signing, and releasing your application.” - https://docs.fastlane.tools/

Setup

In the root of ios and android folders, create a Gemfile with the following content:

source "https://rubygems.org" gem "fastlane"

The AppFile

Fastlane uses this file to grab env variables it needs for the lanes. A lane can be described as a workflow or task.

# For more information about the Appfile, see: # https://docs.fastlane.tools/advanced/#appfile app_identifier(ENV['FLAVOUR_PACKAGE_NAME']) # The bundle identifier of your app itc_team_id(ENV['ITC_TEAM_ID']) # App Store Connect Team ID

The .env file: .env.Test

A sample .env file to use. You could have a different .env file for a different app flavour

FLAVOUR_PACKAGE_NAME = "com.company.app1" SCHEME_NAME = "Production" ITC_TEAM_ID = "TEAM_ID_HERE"

Template for iOS

Use the following template to complete the tasks of

  • clean
  • build
  • sign
  • upload to Apple

This template will use an AWS S3 bucket to store the signing keys.

Uses env variables to allow the template to be used as part of a CICD solution and is not dependent on the type of secret manager used in your pipeline

# This file contains the fastlane.tools configuration # You can find the documentation at https://docs.fastlane.tools default_platform(:ios) before_all do ######### delete build folders. Used by build_app ######### sh "rm -rf ./build" ######### clear derived data ######### clear_derived_data end ######### beta ######### platform :ios do desc "Push a new beta build to TestFlight" lane :beta do |options| begin puts "Apple App Store connect API " app_store_connect_api_key( key_id: options[:key_id], issuer_id: options[:issuer_id], key_content: options[:key_content], duration: 1200 # optional (maximum 1200) ) ######### download and install certs and prov profiles ######### match( type: "appstore", readonly: false, verbose: false, api_key: lane_context[SharedValues::APP_STORE_CONNECT_API_KEY], storage_mode: 's3', s3_region: options[:s3_region], s3_access_key: options[:s3_access_key], s3_secret_access_key: options[:s3_secret_access_key], s3_bucket: options[:s3_bucket], team_id: ENV['ITC_TEAM_NAME'], keychain_password: options[:keychain_password] ) puts "certificates are ok" ######### build and sign .ipa file ######### ipaName = "#{ENV['SCHEME_NAME']}.ipa" build_app( workspace: "App.xcworkspace", scheme: ENV['SCHEME_NAME'], configuration: "Release", clean: true, silent: true, suppress_xcode_output: true, include_bitcode: true, include_symbols: true, output_directory: "./fastlane/build", output_name: ipaName ) puts "app built successfully" ######### verify build ######### verify_build( provisioning_type: "distribution", bundle_identifier: CredentialsManager::AppfileConfig. try_fetch_value(:app_identifier), ) puts "app verified successfully" ######### upload to TestFlight ######### puts "uploading to TestFlight" upload_to_testflight( api_key: lane_context[SharedValues::APP_STORE_CONNECT_API_KEY], notify_external_testers: ENV['DISTRIBUTE_EXTERNAL'] ) puts "uploaded to TestFlight" clean_build_artifacts puts '--success--' rescue => exception puts '************ --exception-- *************' puts exception.to_s # tidy up clean_build_artifacts end end end

Template for Android

Uses env variables to allow the template to be used as part of a CICD solution and is not dependent on the type of secret manager used in your pipeline.

fastlane_require 'dotenv' default_platform(:android) @track = 'internal' @latest_app_version_in_track = 'N/A' before_all do ######### check for any fastlane upates. Minor fixes only ######### # update_fastlane ######### delete build folders, create a new build folder. Used by build_app ######### sh "rm -rf build" sh "mkdir build" sh "rm -rf app/build/*" end ######### closed beta ######### platform :android do desc "Build signed Android app bundle by variant" lane :beta do |options| begin ######### check service account with play store ######### validate_play_store_json_key( json_key_data: options[:service_account_json_key_data] ) puts "build bundle" ######### build bundle ######### build_bundle(keystore_upload_password: options[: keystore_upload_password]) ######### upload to play track ######### upload_to_play_store( json_key_data: options[:service_account_json_key_data], track: @track, skip_upload_apk: true, skip_upload_metadata: true, skip_upload_images: true, skip_upload_screenshots: true, skip_upload_aab: false, skip_upload_changelogs: false ) puts 'success' get_app_version(@track, options[:service_account_json_key_data]) puts @latest_app_version_in_track rescue => exception puts '--exception---' puts exception writeErrorFileToBuildFolder(exception) end end end ######### private lanes ######### platform :android do desc "Build signed app bundle by variant" private_lane :build_bundle do |values| begin puts "let's begin " + ENV['FLAVOUR_NAME'] ######### clean ######### build_android_app( tasks: ["clean"], print_command_output: true ) puts "building app" ######### build the app ######### build_android_app( task: "bundle", build_type: "Release", flavor: ENV['FLAVOUR_NAME'], system_properties: { "ENVFILE": ENV['CONFIG_ENVFILE'] }, properties: { "android.injected.signing.store.file" => ENV ['KEY_STORE_FILE'], "android.injected.signing.store.password" => values[: keystore_upload_password], "android.injected.signing.key.alias" => ENV ['KEY_STORE_ALIAS'], "android.injected.signing.key.password" => values[: keystore_upload_password] }, print_command_output: true ) puts "build complete" puts lane_context[SharedValues::GRADLE_AAB_OUTPUT_PATH] puts "extract .apk from .aab" ######### build the app ######### bundletool( ks_path: ENV['KEY_STORE_FILE'], ks_password: values[:keystore_upload_password], ks_key_alias: ENV['KEY_STORE_ALIAS'], ks_key_alias_password: values[:keystore_upload_password], bundletool_version: '0.11.0', aab_path: lane_context[SharedValues::GRADLE_AAB_OUTPUT_PATH], apk_output_path: "fastlane/build/" + ENV['FLAVOUR_NAME'] + ". apk", verbose: true ) ######### tidy up ######### puts "--success--" rescue => exception puts exception # bubble up raise exception end end end # get the latest app version from Play Store by track def get_app_version(track, service_account_json_key_data) nameArray = google_play_track_release_names( track: track, json_key_data: service_account_json_key_data ) @latest_app_version_in_track = nameArray[0] end
Back to the list