Continuous integration for iOS and Android with Fastlane + Gitlab CI
3 July 2020

- Introduction
- Create the docker container to build Android
- Use GitLab Runner to build iOS
- The use of fastlane
- GitLab CI Instruments
- Conclusions
- References
Introduction
In this tutorial we are going to learn how to set up the continuous integration environment for iOS and Android mobile applications using the tools provided by the GitLab platform. What is continuous integration for? If you have any experience in programming, perhaps you have had a situation where when you downloaded some changes from a repository something stopped working. In a team of multiple people it is quite difficult to control the consistency and quality of the code. Continuous integration can prevent situations of this type. But if you are the only person working on a project, automatic builds and running tests after each commit can be very useful too. With continuous integration, you can notice problems faster and fix them sooner.
Create docker container to build Android
To compile the Android project we are going to use the Docker container. It is an effective and fairly simple way to build and run applications without having to use a real computer. Docker containers are a secure and consistent way to create a testing environment.
FROM openjdk:8
ENV ANDROID_SDK_URL https://dl.google.com/android/repository/sdk-tools-linux-3859397.zip
ENV ANDROID_HOME /usr/local/android-sdk-linux
ENV PATH $PATH=$ANDROID_HOME/tools:$ANDROID_HOME/tools/bin:$ANDROID_HOME/platform-tools:$ANDROID_HOME/build-tools:$PATH
WORKDIR /home
RUN mkdir "$ANDROID_HOME" .android && \
cd "$ANDROID_HOME" && \
curl -o sdk.zip $ANDROID_SDK_URL && \
unzip sdk.zip && \
rm sdk.zip && \
# Download Android SDK
yes | sdkmanager --licenses && \
sdkmanager --update && \
sdkmanager "build-tools;29.0.3" && \
sdkmanager "platforms;android-29" && \
sdkmanager "platform-tools" && \
sdkmanager "extras;android;m2repository" && \
sdkmanager "extras;google;m2repository" && \
# Install Additional Packages
apt-get update && \
apt-get --quiet install --no-install-recommends -y --allow-unauthenticated build-essential vim-common git ruby-full openssl libssl-dev
gem install fastlane && \
gem install bundler && \
# Clean up
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* && \
apt-get autoremove -y && \
apt-get clean
In this docker file, apart from the Android SDK, we also put Ruby gems like Bundler and Fastlane that will help us in the process of uploading our binaries to the online application stores AppStore (iOS) and PlayStore (Android).
We also need to make a couple of changes to our basic Gradle configuration to make publishing easier. In the file app/build.gradle we have to put the variables that will be used to sign our Android application. We can use a file that contains all variables or use GitLab variables. The second is safer.
// Try reading secrets from file
def secretsPropertiesFile = rootProject.file("secrets.properties")
def secretProperties = new Properties()
if (secretsPropertiesFile.exists()) {
secretProperties.load(new FileInputStream(secretsPropertiesFile))
}
// Otherwise read from environment variables, this happens in CI
else {
secretProperties.setProperty("signing_keystore_password", "${System.getenv('signing_keystore_password')}")
secretProperties.setProperty("signing_key_password", "${System.getenv('signing_key_password')}")
secretProperties.setProperty("signing_key_alias", "${System.getenv('signing_key_alias')}")
}
We also have to upload the code version every time we upload the binary. To do this we introduce small changes to the same file:
android {
defaultConfig {
applicationId "im.gitter.gitter"
minSdkVersion 19
targetSdkVersion 26
versionCode Integer.valueOf(System.env.VERSION_CODE ?: 1)
To sign the release version we inject our variables that we introduced before:
signingConfigs {
release {
// You need to specify either an absolute path or include the
// keystore file in the same directory as the build.gradle file.
storeFile file("../android-signing-keystore.jks")
storePassword "${secretProperties['signing_keystore_password']}"
keyAlias "${secretProperties['signing_key_alias']}"
keyPassword "${secretProperties['signing_key_password']}"
}
}
buildTypes {
release {
minifyEnabled false
testCoverageEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.release
}
}
}
Use GitLab Runner to build iOS
Unfortunately, compiling iOS applications in the Gitlab environment requires a physical computer. You cannot use Docker containers or virtual machines. The only solution is to install a GitLab Runner application on your physical computer. It is done quite quickly and easily. Here you can see the instructions for all operating systems. After installing Gitlab Runner you have to register it with a token from your project. You also have to put a tag on it to use it later in the Gitlab CI script. In this way, the tasks linked to iOS will be executed on the computer with the Gitlab Runner installed.
Using fastlane
To use Gitlab CI, we need a way to build and archive our project from terminal. Although we can do this without additional tools using the xcodebuild command, the best solution will be to use the Fastlane tool. If you haven't used Fastlane before, in short, it is a tool that, based on a simple script prepared by us, can automatically perform various operations related to the entire application development process. For example: create a project, run tests, generate code coverage reports, submit builds to TestFlight, HockeyApp, etc., automatically generate application screenshots, send build notifications, and many other operations.
To ensure that we use the same versions of all the necessary tools on each computer, we can use the Bundler tool. You can say that Bundler is somewhat similar to Cocoapods, but instead of a Podfile we have a Gemfile and instead of managing versions of iOS libraries, it is used to manage versions of Ruby gems, for example CocoaPods and Fastlane. So, it's another tool that in some cases makes life easier and helps avoid incompatibility issues between particular versions of Ruby gems.
The problem with different versions of CocoaPods is related to the situation where Pods are not saved in the git repository. If the entire Pods directory is kept in the git repository and you don't have to download external libraries to build your project, then the problem does not occur and you may not need Bundler in this case. Depending on your preferences and needs, you can decide if it is worth using Bundler in your case or maybe it is worth keeping Pods in git. Let's configure Fastlane so we can build and export our project binary from the terminal.
According to the description at https://docs.fastlane.tools/getting-started/ios/setup/ you can install Fastlane with this command:
# Using RubyGems
sudo gem install fastlane -NV
# Alternatively using Homebrew
brew install fastlane
Using "gem install fastlane -NV" may require sudo if RVM is not used on your computer and you have not changed the gem installation path by setting the GEM_HOME environment variable. When the Fastlane installation is finished, you can go to the directory with your project in the terminal and initialize Fastlane with the following command:
fastlane init
After that, a new fastlane directory will be created with a file called Fastfile. Alternatively, instead of fastlane init, you can simply create that directory and file manually. Open that file in any text editor and change its content to the following for iOS:
platform :ios do
desc "Build the application release version"
lane :buildRelease do
build_app(scheme: "App",
workspace: "ios/App/App.xcworkspace",
clean: true,
include_bitcode: true,
output_directory: "."
)
end
end
And for Android:
platform :android do
desc "Builds the debug code"
lane :buildDebug do
gradle(task: "assembleDebug",
project_dir: "./android")
end
desc "Builds the release code"
lane :buildRelease do |options|
gradle(task: "assemble",
flavor: options[:flavor],
build_type: "Release",
project_dir: "./android")
end
end
Now you can test from your computer's terminal if everything is well defined.
fastlane iOS buildRelease
fastlane android buildRelease
Fastlane can also help us upload the binaries to the application stores (markets). For this you have to use the upload_to_play_store command in the case of Android:
desc "Submit a new Internal Build to Play Store"
lane :internal do |options|
upload_to_play_store(track: 'internal', apk: options[:apkPath], version_name: '1.0', package_name: options[:packageName])
end
and in the case of iOS upload_to_test_flight:
desc "Upload build to the testflight"
lane :uploadToTestFlight do |options|
upload_to_testflight(
uses_non_exempt_encryption: false,
distribute_external: true,
changelog: (options[:changeLog] ? options[:changeLog]: "New version"))
end
Both have many parameters but the main ones are the location of the binary (in the case of iOS you can define it in the Gymfile file), the parameters of the internal or external tests, the information for the testers, etc.
Gitlab CI Instruments
We have already configured Gitlab Runner and can build our project from the terminal. It's time to make our project build automatically after every change in git. We need a .gitlab-ci.yml configuration file that we need to create in the main directory of our repository. Then we should put the following content in it:
.upload-iOS-app:
stage: publish-apps
allow_failure: true
tags:
- ios
before_script:
- export LC_ALL=en_US.UTF-8
- export LANG=en_US.UTF-8
when: manual
only:
- master
upload-iOS-dev-app:
extends: .upload-iOS-app
script:
- bundle exec fastlane ios incrementBuildNumber
- bundle exec fastlane ios buildRelease
- bundle exec fastlane ios uploadToTestFlight changeLog:"Dev version"
This job uses the template .upload-iOS-app with the previous fastlane settings such as export LC_ALL=en_US.UTF-8 and export LANG=en_US.UTF-8 and the fastlane commands are executed to increase the build number, compile the release version and upload the ipa (binary) file to TestFlight.
For Android the script is different. We use the Docker container that we have already prepared before. We have to prepare the GitLab variables for our job to work. $signing_jks_file_hex is a hexdump of our Android keystore. Then we convert it with the command «*echo «$signing_jks_file_hex» | xxd -r -p – > ./android/key.keystore»*in the initial file. We upload the version of the app code using the variable $CI_PIPELINE_IID (the id of our pipeline). We take from another GitLab variable $google_play_api_key_json the Google Play Api key that is used to access the Google Play Store. In the end, we delete the files with the keys.
.upload-android-app:
image: docker.eienergia.com/cicd/android-container:0.1.0
stage: publish-apps
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- node_modules
policy: pull
allow_failure: true
when: manual
before_script:
# We store this binary file in a variable as hex with this command, `xxd -p gitter-android-app.jks`
# Then we convert the hex back to a binary file
- echo "$signing_jks_file_hex" | xxd -r -p - > ./android/key.keystore
# We add 1 to get this high enough above current versionCodes that are published
- 'export VERSION_CODE=$((100 + $CI_PIPELINE_IID)) && echo $VERSION_CODE'
- echo $google_play_api_key_json > ./fastlane/playstore_key.json
- bundle update --all
after_script:
- rm ./fastlane/playstore_key.json
- rm ./android/key.keystore
only:
- master
upload-android-dev-app:
extends: .upload-android-app
script:
- bundle exec fastlane android buildRelease flavor:dev
- bundle exec fastlane android internal apkPath:"./android/app/build/outputs/apk/dev/release/app-dev-release.apk" packageName:"com.app"
The “upload-android-dev-app” job launches the “bundle exec fastlane android buildRelease flavor:dev” command to build the application with the “dev” flavor (a build variant). Then bundle exec fastlane android internal is used to upload the binary to the Play Store.
Conclusions
Keep in mind that by default GitLab offers 2,000 free continuous integration minutes and then if you run out of minutes you have to pay. Even so, for small projects it is a great help. You don't have to set up your own continuous integration environment by purchasing and maintaining your own equipment. And if you already have them, you can use them by installing GitLab Runner and managing the entire process from the GitLab website. The GitLab team promises to implement shared runners with Mac OS in the future: GitLab issues. Its main competitor GitHub already has this functionality. Hopefully it will be soon and we can avoid installing GitLab Runner on a real machine. As you can see, setting up a test environment with GitLab CI and Fastlane is quite simple and does not require much time.