Sitemap

Fastlane Release Automation for Xamarin Native Mobile Applications

8 min readMar 28, 2023

--

Press enter or click to view image in full size
Photo by Christina Morillo on Pexels

Introduction

If you’re working as an app developer, you should probably know that an app’s release process can become very challenging — whether it’s a brand new app, or an update to an existing app — especially when compared to releasing updates to web or desktop applications.

First, a mobile app’s new version needs to be reviewed by the platform (App Store or Google Play). In addition, you need to manage and update the certificates and provisioning profiles used for iOS code signing, that expire every year. This adds a burden of an extra manual work on the developer team. To top it all off, if you are developing a cross-platform application (both for iOS and Android platforms), as I do, you need to go through the entire release process twice. Already sounds discouraging, doesn't it?

I am working as a head app developer at Nordic IT, a company specialised in email software development. Our product is called reMARK and it is a powerful email solution aimed at maritime sector. Three years ago, when I started working on the app version of reMARK, the release process was completely manual. In the past we released new versions three or four times a year, therefore manual release management quite satisfied our product needs. However, recently, because of the increasing number of new features and the growing variety of server versions used by our customers, it has become necessary to deliver patch versions to the App Store and Google Play more frequently. Manual releases have become extremely time-consuming. Thus, we have been confronted with the urgent need to find the possibility of automating the process of releasing our mobile applications.

Inspired by ideas from the recent Podlodka iOS Crew conference I decided to use Fastlane toolkit to automate the release process of our mobile applications. Fastlane is a powerful open source toolkit which can help you to improve release efficiency, avoid the burden of frequent release on the team and reduce possible mistakes in a manual release.

The Challenge

The main benefits of using Fastlane are the possibility to integrate with all existing tools and services needed for app release process and the MIT license, that basically allows you to do whatever you want with the source code as long as you include the original copyright and license notice in any copy of the software/source license. However, there are some restrictions to be concerned for projects developed using Xamarin cross-platform framework. Unlike native applications for iOS and Android, Xamarin applications are written in C#, therefore Fastlane actions intended for building binaries are not suitable for Xamarin apps. Xamarin developers have two options to solve this problem:

  • Use existing Xamarin plugin already created by an open source community members that can be easily installed using RubyGems.
  • Create your own plugin written in Ruby language, upload the code to GitHub, create a gem and publish it on RubyGems.

Unfortunately, none of existing Xamarin plugins suited the needs of our project, most of them were outdated and did not work correctly, so the only solution was to implement our own plugin. Fastlane has a very simple and clear instructions on how to create your own plugin. In a nutshell, Fastlane tool creates the plugin folders structure for you and all you need is to write some Ruby code for the actions you need for release automation. Ruby is a fairly simple and intuitive language and if you know some other scripting language like Python you will get the job done. After a little research on the settings of msbuild tool, which is necessary for building .NET projects, I succeeded in creating my own Fastlane Xamarin plugin, uploading it to Github and publishing it on RubyGems. The plugin is used to automate the following steps of our release pipeline for both iOS and Android projects:

  • incrementing version and build numbers
  • restoring NuGet packages
  • building the source code
  • creating and signing bundle archives

At the figure 1 and the figure 2 you can see the detailed workflow diagrams for reMARK app's release pipelines for iOS and Android platforms. The above mentioned plugin automates four steps of those pipelines. All the other steps can be automated by built-in Fastlane actions since they don't depend on Xamarin applications' specifics.

Press enter or click to view image in full size
Figure 1 — reMARK iOS app's release pipeline.
Press enter or click to view image in full size
Figure 2— reMARK Android app’s release pipeline.

The Setup Process

To deploy a Xamarin app via Fastlane, you need to install Fastlane first. Execute the following command to install Fastlane.

brew install fastlane

Then you need to go to your solution folder and initialize Fastlane using the following command.

fastlane init

After the initialisation, you can see the folders and files are created in the solution. The structure should look as follows.

|- fastlane
| |- Appfile
| |- Fastfile
|- Gemfile
|- Gemfile.lock

The most interesting file is fastlane/Fastfile, which contains all the information that is needed to distribute your app. The next file fastlane/Appfile contains the settings needed for the AppStore and Google Play authorisation. If you are going to automate certificates and provisioning profiles management, you will need to add another file called fastlane/Matchfile, which contains the settings for your certificates repository. Gemfile and Gemfile.lock manage the dependencies of Ruby for Fastlane.

Get Anna Gulich’s stories in your inbox

Join Medium for free to get updates from this writer.

In order to install the Xamarin plugin you need to execute the following command.

fastlane add_plugin fastlane-plugin-xamarin_native

This command will add fastlane/Pluginfile, with the following contents.

# Autogenerated by fastlane
#
# Ensure this file is checked in to source control!
gem 'fastlane-plugin-xamarin_native'

fastlane/Pluginfile specifies the plugins used by your Fastlane scripts, so once you have this file in your fastlane folder, you can install all the required plugins by executing the following command.

fastlane install_plugins 

Once you have your plugin set up, you can call the actions declared in the plugin in your fastlane/Fastfile. For example, in my fastlane/Fastfile the following code snippet calls four actions declared in the Xamarin plugin:

  • bump_version — for incrementing the version;
  • bump_code — for incrementing the build number;
  • nuget — for restoring NuGet packages;
  • msbuild — for building the source code and creating an ipa archive.
 desc 'Build iOS app release'
lane :build_release do |options|
begin
bump_version(platform: 'iOS', type: options[:version_type], info_plist_pathes: ENV['INFO_PLIST_PATHES'])
bump_code(platform: 'iOS', info_plist_pathes: ENV['INFO_PLIST_PATHES'])
update_entitlements
generate_certificates
nuget(
solution: 'Mark5.sln',
verbosity: 'detailed'
)
msbuild(
solution: './Mark5.sln',
platform: 'iPhone',
target: options[:target],
build_type: 'Release',
ipa_name: 'remark.ipa'
)
slack_on_success
rescue StandardError => e
slack_on_error(e)
end
end

In order to store the environment variables you can set up a fastlane/.env.default file. All the variables declared in this file can be used in your fastlane/Fastfile by calling ENV['YOUR_VARIABLE_NAME'].

Here is the full code of myfastlane/Fastfilescript.

# 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
require 'nokogiri'

default_platform(:ios)

platform :ios do

#region Certificates
desc 'Generate certificates'
lane :generate_certificates do
sync_code_signing(
type: 'development',
app_identifier: ['com.nordic-it.mark5.mobile.ios',
'com.nordic-it.mark5.mobile.ios.callid',
'com.nordic-it.mark5.mobile.ios.share'],
force_for_new_devices: true,
readonly: false
)
sync_code_signing(
type: 'appstore',
app_identifier: ['com.nordic-it.mark5.mobile.ios',
'com.nordic-it.mark5.mobile.ios.callid',
'com.nordic-it.mark5.mobile.ios.share'],
readonly: false
)
sync_code_signing(
type: 'adhoc',
app_identifier: ['com.nordic-it.mark5.mobile.ios',
'com.nordic-it.mark5.mobile.ios.callid',
'com.nordic-it.mark5.mobile.ios.share'],
force_for_new_devices: true,
readonly: false
)
end
#endregion

#region Versioning
def version
get_info_plist_value(path: './Mark5.Mobile.IOS/Info.plist',
key: 'CFBundleShortVersionString')
end

def build_number
get_info_plist_value(path: './Mark5.Mobile.IOS/Info.plist',
key: 'CFBundleVersion')
end
#endregion

#region Build
def update_entitlements
update_plist(
plist_path: './Mark5.Mobile.IOS/Entitlements.plist',
block: proc do |plist|
plist[:"aps-environment"] = 'production'
plist[:"com.apple.developer.icloud-container-environment"] = 'Production'
end
)
rescue StandardError => e
slack_on_error(e)
end

desc 'Build iOS app release'
lane :build_release do |options|
begin
bump_version(platform: 'iOS', type: options[:version_type], info_plist_pathes: ENV['INFO_PLIST_PATHES'])
bump_code(platform: 'iOS', info_plist_pathes: ENV['INFO_PLIST_PATHES'])
update_entitlements
generate_certificates
nuget(
solution: 'Mark5.sln',
verbosity: 'detailed'
)
msbuild(
solution: './Mark5.sln',
platform: 'iPhone',
target: options[:target],
build_type: 'Release',
ipa_name: 'remark.ipa'
)
slack_on_success
rescue StandardError => e
slack_on_error(e)
end
end
#endregion

#region Slack
def slack_on_success(message)
slack(
slack_url: ENV['SLACK_URL'],
message: message,
channel: ENV['SLACK_CHANNEL'],
success: true,
attachment_properties: { fields: [{ title: 'Version', value: get_version },
{ title: 'Build number', value: get_build_number }] }
)
end

def slack_on_error(exception)
slack(
slack_url: ENV['SLACK_URL'],
message: 'Error occured ☹️!',
channel: ENV['SLACK_CHANNEL'],
success: false,
attachment_properties: { fields: [{ title: 'Error message', value: exception }] }
)
end
#endregion

#region Deliver
desc 'Upload to Testflight'
lane :release_beta do |options|
begin
groups = options[:type] == 'internal' ? ['internal'] : ['all']
app_store_connect_api_key
pilot(
changelog: options[:changelog],
distribute_external: true,
distribute_only: false,
ipa: './Mark5.Mobile.IOS/bin/iPhone/Release/remark.ipa',
groups: groups,
skip_submission: false,
skip_waiting_for_build_processing: false
)
slack_on_success('reMARK iOS app successfully uploaded to Testflight 📱')
rescue StandardError => e
slack_on_error(e)
end
end

desc 'Upload to AppStore'
lane :release do
begin
deliver(
api_key: app_store_connect_api_key,
app_version: version,
app_identifier: 'com.nordic-it.mark5.mobile.ios',
ipa: './Mark5.Mobile.IOS/bin/iPhone/Release/remark.ipa',
submit_for_review: true,
automatic_release: true,
phased_release: true,
force: true, # Skip HTMl report verification
skip_metadata: false,
skip_screenshots: true,
skip_binary_upload: false,
run_precheck_before_submit: false,
submission_information: (ENV['SUBMISSION_INFORMATION'])
)
slack_on_success('reMARK iOS app successfully uploaded to AppStore 📱')
rescue StandardError => e
slack_on_error(e)
end
end
#endregion
end

platform :android do

#region Versioning

def version
File.open(ENV['ANDROID_MANIFEST_PATH']) do |f|
@doc = Nokogiri::XML(f)
current_version = @doc.at('//manifest/@android:versionName').text
return current_version.to_s
end
end

def build_number
File.open(ENV['ANDROID_MANIFEST_PATH']) do |f|
@doc = Nokogiri::XML(f)
current_build = @doc.at('//manifest/@android:versionCode').text
return current_build.to_s
end
end

#endregion

#region Build

desc 'Build Android app release'
lane :build_release do |options|
begin
bump_version(platform: 'Android', type: options[:version_type], manifest_file_path: ENV['ANDROID_MANIFEST_PATH'])
bump_code(platform: 'Android', manifest_file_path: ENV['ANDROID_MANIFEST_PATH'])
nuget(
solution: 'Mark5.sln',
verbosity: 'detailed'
)
msbuild(
project: './Mark5.Mobile.Droid/Mark5.Mobile.Droid.csproj',
target: 'Build',
build_type: options[:target],
sign_apk: true,
android_signing_keystore: ENV['PATH_TO_KEYSTORE'],
android_signing_key_alias: 'default',
android_signing_key_pass: '********',
android_signing_store_pass: '********'
)
rescue StandardError => e
slack_on_error(e)
end
end

#endregion

#region Deliver
desc 'Upload beta release to Google Play beta'
lane :release_beta do
begin
supply(
apk: './Mark5.Mobile.Droid/bin/Release/com.nordic_it.mark5.android-Signed.apk',
track: 'beta',
skip_upload_images: true,
skip_upload_screenshots: true,
package_name: 'com.nordic_it.mark5.android',
version_name: version,
version_code: build_number
)
slack_on_success('reMark Android app was succcessfully uploaded to Google beta')
rescue StandardError => e
slack_on_error(e)
end
end

desc 'Upload release to Google Play'
lane :release do
begin
supply(
apk: './Mark5.Mobile.Droid/bin/Release/com.nordic_it.mark5.android-Signed.apk',
track: 'rollout',
rollout: '0.5',
skip_upload_images: true,
skip_upload_screenshots: true,
package_name: 'com.nordic_it.mark5.android',
version_name: version,
version_code: build_number
)
slack_on_success('reMark Android app was succcessfully uploaded to Google beta')
rescue StandardError => e
slack_on_error(e)
end
end
#endregion
end

Now, to build a new patch version for iOS or Android application we just need to call one of the following commands from the solution folder.

# for ios
fastlane ios build_release version_type:"patch" target: "Rebuild"

#for android
fastlane android build_release version_type:"patch" target: "Rebuild"

For delivering a public beta version to TestFlight or Google Play Beta we need to execute one of the following commands.

# for ios
fastlane ios release_beta type: "all" change_log: "fixed bugs"

#for android
fastlane android release_beta

For delivering a new release to the AppStore or Google Play the commands are as follows.

# for ios
fastlane ios release

#for android
fastlane android release

Conclusion

Fastlane is an amazingly powerful release automation tool. It has many built-in actions that allow you to automate routine tasks such as certificate management, building binaries, creating and signing bundle archives, working with stores, sending messages to Slack and much more. Moreover, thanks to the MIT license, you can expand its capabilities by developing your own plugins.

As for our project, previously creating a new release for two platforms took about two hours of a developer’s time. Now the process takes about 30 minutes and requires absolutely no interaction with the developer. Thanks to Fastlane, the release process has become a pleasant experience!

--

--

Anna Gulich
Anna Gulich

Written by Anna Gulich

0 followers

Software developer