Continuous Integration/Deployment for mobile

The mobile software development (aka Apps Development) became more and more specific in this period, as far as became an entire department with (sometimes) hierachy and open positions. This brought the need of common software development pratices like Continuous Integration and Continuous Deployment.

The continuous integration is pretty standard with tests for the “business logic” and many others, like end to end tests with a simulated backend and so on and so forth. The continuous deployment, instead, consist in the distribution of an update through all the platform (iOS, Android, and sometimes Windows) stores.

So far everthing seems a standard procedure, but among the jungle of mobile development frameworks (ionic, flutter, native development) is not! If the test part could be easily made using one of the test frameworks for the selected platform (in combination with the development framework), btw good luck with that, the deployment part requires some manual procedures and validation triggers.
Also to deploy in “production” the applications you have to produce a lot of legal documentation (GDPR), and beyond that the developers have to produce screenshots of the application on a subset of devices. Fortunately there is a tool which, beyond the legal stuff which is on the company/developer own, there is a tool which cover all the distribution part including screenshots (from the latest releases): Fastlane.

Fastlane: quick and clean

Fastlane is a build tools for fast release of the application, which cover also all the side aspects of application publishing lifecycle and test running (only for apps writtent with the native SDK).
Fastlane could build your application using traditional build methods, like traditional build method for Android spawning a Gradle daemon or using xcodebuild for iOS, but it also support cross development platform like React Native and Flutter; it supports also Ionic (fortunately!) using third party plugins.
It could also, with some integrations in terms of software, perform other tasks like screenshots, beta (and also alpha for Android) deployment, and automatic code signing for iOS.

Get Started with Fastlane

Fastlane requires that you have already created the project(s) for your app on the stores, to do that there are a lot of articles, personally I suggest the one from themanifest for Android and a Medium article from the same author for iOS.
Then after the setup (assuming that you have already installed fastlane) you can move into your app directory (cd <your-app-directory>) and then run fastlane init; once done that you have a file called Fastfile where you can setup your lanes and another file called Appfile. The Appfile contains all your credentials for your app deployment, like keystore (with password), or Apple Store ID for automatic code sign and upload; of course the developer must setup the enviroment, using the guides for iOS and Android.
The Fastfile contains all the lane definitions for the application, they could be “specialized” for each platform (in case of cross platforms). Each lane contains a set of actions, the developer could choose among the native and the one introduced by plugins; for instance:

lane :playstore do
  gradle(
    task: 'assemble',
    build_type: 'Release'
  )
  upload_to_play_store # Uploads the APK built in the gradle step above and releases it to all production users
end

this lane will build your android application and, if everything went fine, upload the APK to play store “in production” (where production means the release branch of the store).

Fastlane features

Fastlane supports side tasks in mobile app distribution like screenshots for iOS and Android, leveraging on emulators for both platforms and other third party software, this procedure could be automated in the FastFile using specific actions.

lane :screenshots do
  capture_screenshots
  upload_to_app_store
end

For iOS is pretty straight forward because everything is integrated with the development platform, on Android is different, see the guide

lane :screenshots do
  capture_android_screenshots
  upload_to_play_store
end

The Android screenshots part is require screengrab to take screenshots from mobile emulator.
Fastlane originally was designed to support the entire lifecycle of native-developed mobile applications, in fact the developer could also run tests.

lane :tests do
  gradle(task: "test")
end

The above lane show how to run tests on Android, for iOS, instead, it relies on a custom action because the build process is sliced in different tools.

lane :tests do
  run_tests(workspace: "Example.xcworkspace",
            devices: ["iPhone 6s", "iPad Air"],
            scheme: "MyAppTests")
end

When hybrid/cross development platforms began to become popular, and also to support integration with other horizontal platforms for developers (like slack, hipchat and so on) the Fastlane developers defined a plugin system. Each plugin define custom actions, for instance you can send a message to a slack channel, to be support other platforms or system.
For instance with the gmail plugin the developer could send a short report using gmail, but there are a tons of plugins to upload file to slack, or handle firebase, handle version number and so on and so forth.

Hybrid platforms

Although Fastlane has a huge support to native development, it could handle also a wide variety of hybrid platforms. Platforms like Flutter and React Native are supported using the normal build process, more details here for Flutter and here for React Native, Xamarin and Ionic/Cordova, instead, are supported through plugins (ionic and xamarin).

Gitlab CI for mobile applications

Gitlab CI is an embedded CI/CD pipeline tool, it leverage on runners to perform tasks. In order to define a proper pipeline the developer must include a file named .gitlab-ci.yml in the repository.
Runner allows to run:

  • Multiple jobs concurrently.
  • Use multiple tokens with multiple server (even per-project).
  • Limit number of concurrent jobs per-token. Each runner jobs can be run:
  • Locally.
  • Using Docker containers.
  • Using Docker containers and executing job over SSH.
  • Using Docker containers with autoscaling on different clouds and virtualization hypervisors.
  • Connecting to remote SSH server. The runner:
  • Is written in Go and distributed as single binary without any other requirements.
  • Supports Bash, Windows Batch, and Windows PowerShell.
  • Works on GNU/Linux, macOS, and Windows (pretty much anywhere you can run Docker).
  • Allows customization of the job running environment.
  • Automatic configuration reload without restart.
  • Easy to use setup with support for Docker, Docker-SSH, Parallels, or SSH running environments.
  • Enables caching of Docker containers.
  • Easy installation as a service for GNU/Linux, macOS, and Windows.
  • Embedded Prometheus metrics HTTP server.
    (credits Gitlab Runner documentation)

The runner could be configured using the toml file, see gitlab runner advanced configuration here for details, each runner could be registered multiple times with different providers; for instance a runner on macOS could accept shell tasks (for mobile app build) and also docker build stuff, a windows runner could execute shell tasks and docker (for windows containers) build.

Gitlab CI files and instructions

The CI pipeline must be defined using a yaml configuration file, which allows define jobs, prepare the environment, perform other steps after each job or at the end of the pipeline.
The developer could select which type of runner use in the CI build, using keywords like image, services or even tags to give some “hints” to the Gitlab CI job scheduler; if there aren’t any feasible runner the job will remain in pending state. The CI jobs could be triggered only for special branches, or for some references (or tags) could perform deployment tasks.
For instance this is an example (taken from Gitlab blog) of CI file for Android:

image: openjdk:8-jdk

variables:
  ANDROID_COMPILE_SDK: "28"
  ANDROID_BUILD_TOOLS: "28.0.2"
  ANDROID_SDK_TOOLS:   "4333796"

before_script:
  - apt-get --quiet update --yes
  - apt-get --quiet install --yes wget tar unzip lib32stdc++6 lib32z1
  - wget --quiet --output-document=android-sdk.zip https://dl.google.com/android/repository/sdk-tools-linux-${ANDROID_SDK_TOOLS}.zip
  - unzip -d android-sdk-linux android-sdk.zip
  - echo y | android-sdk-linux/tools/bin/sdkmanager "platforms;android-${ANDROID_COMPILE_SDK}" >/dev/null
  - echo y | android-sdk-linux/tools/bin/sdkmanager "platform-tools" >/dev/null
  - echo y | android-sdk-linux/tools/bin/sdkmanager "build-tools;${ANDROID_BUILD_TOOLS}" >/dev/null
  - export ANDROID_HOME=$PWD/android-sdk-linux
  - export PATH=$PATH:$PWD/android-sdk-linux/platform-tools/
  - chmod +x ./gradlew
  # temporarily disable checking for EPIPE error and use yes to accept all licenses
  - set +o pipefail
  - yes | android-sdk-linux/tools/bin/sdkmanager --licenses
  - set -o pipefail

stages:
  - build
  - test

lintDebug:
  stage: build
  script:
    - ./gradlew -Pci --console=plain :app:lintDebug -PbuildDir=lint

assembleDebug:
  stage: build
  script:
    - ./gradlew assembleDebug
  artifacts:
    paths:
    - app/build/outputs/

debugTests:
  stage: test
  script:
    - ./gradlew -Pci --console=plain :app:testDebug

This CI define 2 main stages, build and test, in the first one it will perform some lint task and then build for debug purposes the application, the test stage instead is used to run tests. The before script session will be performed before every job in order to prepare the environment. The entire build/test process will be performed in docker containers.

Combine fastlane and Gitlab: use case with Ionic

Although, as discussed previously, Fastlane is mainly designed for natvie mobile app development it supports also third part frameworks like Ionic through its plugin mechanism; combined with Gitlab CI pipelines, the automation process will be very straightforward.
First of all the developer have to do the Fastlane setup process for all the platform that he wants to support (Android and/or iOS) producing the Fastfile and the Appfile, personally I prefer to group all the Fastlane-relative files in a single folder on the root of the application project; another step that has to be done before proceed, or at least before the first release in production/beta/alpha round, is the merge of the AppFile for the credentials of Android and iOS. A simple example of merged Appfile could be:

# Apple Account configuration
app_identifier("<your-bundle-identifier>") # The bundle identifier of your app
apple_id("<your-apple-id>") # Your Apple email address

itc_team_id("<itc-team-id>") # App Store Connect Team ID
team_id("<team-id>") # Developer Portal Team ID

# Android account configuration
package_name("<your-package-name>") # e.g. com.krausefx.app
# For more information about the Appfile, see:
#     https://docs.fastlane.tools/advanced/#appfile

Then we could install the desidered ionic plugin using fastlane add_plugin ionic, this will produce a PluginFile like this:

# Autogenerated by fastlane
#
# Ensure this file is checked in to source control!

gem 'fastlane-plugin-ionic'

# Gems could be installed also from github/gitlab or different paths

The Pluginfile must be checked in to your SCM (in this case a Gitlab repository), so each developer could run fastlane install_plugin to install all the plugins used in the Fastfile.
Now we can define a Fastfile to define each step of the build and release project.

platform :android do
  desc "Build the Android stuff in order to check if everything is correct"
  lane :build do
    ionic(platform: 'android', release: false, device: false)
  end

  desc "Upload to alpha channel"
  lane :alpha do
    current_dir=Dir.pwd
    ionic(platform: 'android', device: false, keystore_path: current_dir+'/my-release-key.keystore', keystore_password: ENV['STORE_PASS'], keystore_alias: '<your-key-alias>')
    upload_to_play_store(track: 'alpha', json_key:current_dir+'/api.json', apk: 'platforms/android/app/build/outputs/apk/release/app-release.apk')
  end

  desc "Upload to beta channel"
  lane :beta do
    current_dir=Dir.pwd
    ionic(platform: 'android', device: false, keystore_path: current_dir+'/my-release-key.keystore', keystore_password: ENV['STORE_PASS'], keystore_alias: '<your-key-alias>')
    upload_to_play_store(track: 'beta', json_key:current_dir+'/api.json', apk: 'platforms/android/app/build/outputs/apk/release/app-release.apk')
  end

  desc "Deploy a new version to the Google Play"
  lane :deploy do
    current_dir=Dir.pwd
    ionic(platform: 'android', device: false, keystore_path: current_dir+'/my-release-key.keystore', keystore_password: ENV['STORE_PASS'], keystore_alias: '<your-key-alias>')
    upload_to_play_store(json_key:current_dir+'/api.json', apk: 'platforms/android/app/build/outputs/apk/release/app-release.apk')
  end
end

platform :ios do

  desc "Build iOS in order to check if everthing is fine"
  lane :build do
    ionic(platform: 'ios', type: 'development', device: false, release: false)
  end

  desc "Upload to testflight"
  lane :beta do
    cert
    sigh
    ionic(platform: 'ios', device: false)
    ipa_path=gym(project:'./platforms/ios/<your-app-name>.xcodeproj',codesigning_identity:'<Code-signin-identity>')
    testflight(skip_waiting_for_build_processing: true, ipa: ipa_path)
  end

  desc "Upload to production"
  lane :deploy do
    cert
    sigh
    ionic(platform: 'ios', device: false)
    ipa_path=gym(project:'./platforms/ios/<your-app-name>.xcodeproj',codesigning_identity:'<Code-signin-identity>')
    deliver(ipa: ipa_path)
  end
end

The above Fastfile define a release process for Android alpha, beta and production stages, instead for iOS you can define just the Testflight and release lanes. The above process could be easily integrated with also the screenshot part but the Ionic plugin does not allow to run tests; to run tests you have to rely on other plugins for interaction with shell, and use ionic cli to run them.
The integration with Gitlab CI is pretty easy, but in order to build and release the iOS version you have to register a gitlab-runner which runs on macOS (you can use a hosting service like xCloud) and you can select it in the CI file using tags. The macOS runner must be configured to run shell tasks because there are not any macOS docker container. This is my CI file for the my Ionic Application:

image: cmaiorano/ionic-builder

stages:
  - build
  - publish

cache: # Working with runner cache
  untracked: true
  key: "$CI_PROJECT_ID"
  paths:
    - node_modules/

build_android:
  stage: build
  before_script:
    - npm i
  script:
    - fastlane android build

build_ios:
  stage: build
  before_script:
    - npm i
    - fastlane install_plugins
  script:
    - fastlane ios build

beta_ios:
  stage: publish
  only:
    - develop
    - /^feature-.*$/
  before_script:
    - npm i
    - fastlane install_plugins
  script:
    - fastlane ios beta  
  tags:
    - macosx

alpha_android:
  stage: publish
  only:
    - /^feature-.*$/
  before_script:
    - npm i
  script:
    - fastlane android alpha


beta_android:
  stage: publish
  only:
    - develop
  before_script:
  - npm i
  script:
    - fastlane android beta

release_ios:
  stage: publish
  only:
    - master
  before_script:
  - npm i
  - fastlane install_plugins
  script:
    - fastlane ios deploy
  tags:
    - macosx

release_android:
  stage: publish
  only:
    - master
  before_script:
  - npm i
  script:
    - fastlane android deploy

The image used for CI was developed for build Android application using the Android SDK plus the ionic environment and Fastlane with Ionic plugin. The above gitlab-ci file does not respect the Git-Flow idea but takes into account an hybrid application development and distribution.