The Widlarz Group Blog

Build and release your app with Fastlane!

January 05, 2022



react native


continuous deployment


Building and deploying mobile applications for both platforms (iOS and Android) can be boring and time-consuming. In this tutorial you will learn how to set up Fastlane and automate the whole process. I assume you are a MacOS user, because fastlane only has partial support on windows, and if you are making iOS apps, you must use MacOS anyway.

It will cover your environment configuration, getting all the credentials necessary for TestFlight and playstore, and of course writing proper lanes for both systems. I didn’t want it to be too long, so I decided to discuss github actions setup in another article. That said, local use of fastlane will still be a huge boost to your app deployment speed.


Environment Configuration

To work with fastlane you need to install Ruby and Fastlane globally on your computer. Chances are you already have the former installed. You can check it by typing Ruby -v in the terminal. If not, visit official-docs. It can be also installed using a package manager such as Homebrew by typing brew install ruby-install or even brew install fastlane. For more detailed information, see the official fastlane-docs.

Authenticate iOS

Because fastlane will push your builds straight to TestFlight or App store, authentication is required. There are several ways to do this, but one recommended by the documentation (and my favourite) is using App store connect api key. If you have not created your project on App-Store-Page yet, you can visit their docs and follow it.

Now we can log in to the App Store page. Next, go to the Users and Access and click Generate Api key. You must enter the name and choose the role key-in-details. Then click “Download Api key” next to it. From this page, we will also need an Issuer ID and Key ID. Issuer ID is above the table and Key ID is in it, in the Key ID column.

All these informations is necessary to connect with the app store lane which we’ll discuss later. For now we can just store them in an .env file.

Create a folder called “fastlane” in your project root folder. Inside, create an .env.default file and add all the required credentials. Don’t forget to add .env.default to the .gitignore.

KEY_ID=Your Key ID
ISSUER_ID=Your Issuer ID
IOS_AUTH_KEY=Paste here content of downloaded App store connect api key with .p8 extension

// fastlane/.env.default

You may encounter some issues with storing .p8 key like this due to formatting. To avoid them you could either add ‘\n’ in place of each new line or convert it to the Base64 format. I personally decided to add newline tags, but feel free to convert it (you can use Homebrew coreutils or openssl).

TestFlight username is simply your account login, and you can find app identifier in XCode under the General or Signing & Capabilities tabs. It can also be obtained by the site, just visit an apple-dev-website and log in. Now go to the Identifiers -> App IDs, find the one you want by name and copy its ID.

This is all the information fastlane needs for iOS authentication. Now we can move to the Android authenticate part.

Authenticate Android

Android doesn’t require as many steps as iOS. All we need is package_name and json_key_file. That’s because getting a json key is well described in the fastlane docs, so there’s no point rewriting it here, just visit docs and follow the nine steps under the Collect Google Credentials header. The package name is visible in the Google Play console in the Dashboard. The format is com.appName.

Now let’s add the package_name to the .env.default file, and copy the generated and downloaded key to the ./android folder.


// fastlane/.env.default

Fastlane ENVs configuration

By default fastlane wouldn’t be able to read your .env.default. That’s why we have to install and configure a ruby plugin. Type in your console

gem install dotenv

Create a file called Gemfile in your root folder and paste this code

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

source ""

gem 'fastlane'
gem "cocoapods"

plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile')
eval_gemfile(plugins_path) if File.exist?(plugins_path)

Gem in ruby is just a library, for our app we would need only these two and some config. Running fastlane command later will install them.

Now that we have all the data we need for authentication and we have read our env file, we can finally start the most important part - writing lanes that will automate our process.

Run before all lanes

Before we write the main lanes, it’s worth adding a few lines of code at the beginning. They will check a few things for us before launching our lanes. In your ./fastlane folder create a file called Fastfile (without any extension). At the top of this file, we need to specify the version of the installed fastlane - you can check it by typing this command in your terminal.

fastlane -v 

Next we can add our lanes that will run before every fastlane run.

before_all do
   yarn(command: "install", step_name: "install_dependencies")

The first lane will install the packages specified in the package.json. The second will check if our actual branch is master. The next one will check if our branch is clean - all changes are committed. If any of them fail, it will throw an error and stop the fastlane. Otherwise the last lane will be executed and it will pull the latest code from our repository. If you are using SSH it will ask for a password to your key.

That’s all for this part. In the next section we will write a few lanes to deploy our app to the TestFlight.

Lanes for iOS

Because we are keeping all lanes for both systems in one file, we have to first specify which lanes are for which systems.

App store connect

platform :ios do

   desc "connect to App store api"
   lane :connect_to_app_store do
         key_id: ENV["KEY_ID"],
         issuer_id: ENV["ISSUER_ID"],
         is_key_content_base64: false,
         key_content: ENV["IOS_AUTH_KEY"],

The description is just plain text which may be helpful to others reading your code. Usually, as in this case, the lane name tells us a lot, so depending on your preferences you can remove the “desc”. The text after ”:” specifies the name of the lane which will later be used in another lane, or can be run by typing fastlane ios connect_to_app_store.

This lane requires the authentication data which we gathered before. Here, if you choose before to convert an IOS_AUTH_KEY to the base64 format, you have to change is_key_content_base64 to true.

I encourage you to check if this lane is working correctly before moving further. Just run fastlane ios connect_to_app_store in the terminal while you’re in the project folder. This will tell you if the current setup is correct and if ENV data is loaded correctly

App version update

Next we need to update the version number of our application. In iOS it’s simple, we just need two functions with a path to the xcodeproj file as arguments.

 desc "Update Version number"
      private_lane :update_version do

       increment_build_number(xcodeproj: "./ios/projectName.xcodeproj")
       increment_version_number( xcodeproj: "./ios/projectName.xcodeproj") 

The first action in this lane increments the build number by one, at least if you don’t pass the build number as an argument. By default, it will change our build number from 3 to 4, but with an argument build_number: 99 the outcome will be 99.

The build number is updated in the project.pbxproj and info.plist files. Here we can also pass the argument skip_info_plist and the latter will be omitted.

Incrementversionnumber is similar, by default increment the last number by one. For example from 1.0.3 to 1.0.4. The New version number can also be passed manually and it will be bumped to the one specified. One additional option is bump_type, which determines if it’s a patch, minor, or major bump. More about semantic versioning

//Example bump with arguments

increment_version_number( xcodeproj: "./ios/projectName.xcodeproj", version_number: 99) // or 
increment_version_number( xcodeproj: "./ios/projectName.xcodeproj", bump_type: "major")  

This action updates only the version_number in ./ios/info.plist file.

If you want more information, see the official documentation.

As you can see, this lane is called private. This means it can only be called from another lane. Right now you can’t run the fastlane update_version, but later we will put it in another lane. It is good practice to use private_lane when there is no reason to invoke it separately. Like in this example - you don’t want to bump the version if you aren’t deploying the application. It will also hide this lane from fastlane lanes, list, and docs.

Build App

We are approaching the end of the iOS lanes. The penultimate lane is the one that will build our application. It uses the two lanes we wrote earlier - connect_to_app_store and update_version, and two new ones from fastlane.

desc "Build the iOS application."

      lane :build do
            podfile: "./ios/Podfile",
            repo_update: true
        gym(scheme: "projectName", workspace: "./ios/projectName.xcworkspace")

The first action named cocoapods simply runs the pod install in your project. All it needs is the podfile path as argument, but we also pass repo_update as true, so it will have the same result as if we went to the ./ios folder and ran pod install —repo-update from the terminal.

The two next ones are already well-known. They connect us to the app store and bump our application version.

The gym action builds and packages the iOS app. Here we pass a schema and workspace path as arguments, but this action has much more configurable parameters. Gym Docs.

Upload to testflight

The last step in the iOS part is uploading our app to the TestFlight.

desc "Ship to Testflight."
        lane :upload do
            upload_to_testflight(username:ENV["TF_USERNAME"], app_identifier:ENV["APP_IDENTIFIER"], skip_submission: true,
            skip_waiting_for_build_processing: true)

In this lane we use our build lane and upload_to_testflight action with some configuration. Because we are already connected to the app store, we can only pass testflight username and app_identifier. This will be enough to build and upload the application.

With skip_submission and skip_waiting_for_build_processing we are disabling the distribution of the build to the testers. It will only upload it to the TestFlight. We need to wait for a while for the build to appear in the TestFlight. This action also has much more configurable parameters; a complete list can be found in the fastlane documentation. Here’s the link to the action uploadtoTestFlight.

The last thing is not obligatory but it sure is useful in order to omit writing fastlane iOS upload every time. You can add a new script in package.json.

   "ios:upload": "fastlane ios upload",

// package.json

With this script you can only run yarn ios:upload or npm run ios:upload. Wait a few minutes and have your app live on TestFlight.

It was a piece of good work that will save you a lot of time with every new build. Let’s move on to the Android lanes.

Lanes for Android

Android doesn’t have a predefined lane for bumping the version name, so we have to do some magic here. First we bump the version in package.json and use this updated value to bump the version name in the Android files.

In the future, if we were to use Github action, we can just get the version from the tag like this:

${{ github.event.staging(build_type).tag_name }}

Then we will have one source of truth for Android and iOS which is a good practice. But for now, let’s stick to the solution without GH actions

 desc "Bump version in package"
   private_lane :bump_version_package do
      sh("npm", "version", "patch", "--no-git-tag-version")

This lane will use npm to update our version name in package.json.

The patch parameter means that the last digit of the version will be updated. Same as iOS, it can be changed to minor or major. Now that we have bumped up the version, we need to use this value in the next lane which will modify Android files.

But first we have to install a plugin which is not in fastlane by default. This can be done by typing the below command in your terminal.

fastlane add_plugin increment_version_code 

Now we can use it in our next private lane.

desc "Bump build version"
   private_lane :bump_build_version do
      package = load_json(json_path: "./package.json")
      increment_version_code(gradle_file_path: "android/app/build.gradle")

         gradle_file_path: "./android/app/build.gradle",
         version_name: package['version']


First we have to run the action for bumping the version name in package.json and then save the path to it as a package. Next, this lane will run two actions which will update our version code and version name. Increment version code can be done simply by passing the gradle file path. The increment version name is the action for which we’re pulling the version name. Here we pass the gradle file path, but also version_name from package.json.

Because building an Android app is much simpler than the iOS one, we are getting to the end of this part. All that is left is pull all the lanes in one, run a few gradle actions, and finally upload the build to the play store

desc "Android build and release to internal"
   lane :upload do
      gradle(task: "clean", project_dir: "./android/")
      gradle(task: "bundle", build_type: "Release", project_dir: "./android/")

         package_name: ENV["ANDROID_PACKAGE_NAME"],
         track: "internal",
         json_key: "./android/app/google-private-key.json",
         aab: "./android/app/build/outputs/bundle/release/app-release.aab"

Important information here is that your Google account needs specific permissions to be able to push the build. As in the documentation, I recommend granting all permissions. More details in the provided link.

As you can probably guess, first we have to bump the version code and version name by running bump_build_version. Then we’ll use the gradle clean task, which will clean up files from old builds - these files may be out of date, and we don’t want them in the new build. The gradle bundle action just builds our application and saves the .aab file. In this action we have to specify the build_type, but in most cases it will just be “Release” or “Staging” / ""QA"" type. Both of these gradle actions need to know the location of the root directory of the gradle project so we pass it as project_dir.

Now that we have everything prepared, we can run the last Android action called upload_to_play_store. Here we will need packagename from the .env file and jsonkey which we gathered in the “Android Authenticate” part. We pass four things as the parameters for this action:

  • package_name -> the name of our package that we stored in .env file.
  • track -> Here we set which user group can see your app. In our case it is the internal group, which means that only internal testers added in Google Play Console can use this app. More about tracks.
  • json_key -> The authentication key downloaded from the Google Play Console, to which we are passing a path. The key itself should be added to the .gitignore file. You can encrypt it, push only this version, and give the password to your coworkers to decrypt it.
  • aab -> The path to our build file. By default it is the one visible above. The path may differ depending on build_type. So for Staging type it will be: “./android/app/build/outputs/bundle/staging/app-staging.aab”, and so on for the other types.

Same as for iOS, we can add a script to package.json for this lane.

   "android:upload": "fastlane android upload",

// package.json


And that’s all. I think that it’s not so much work given the possible time save. Of course there are many things that can be done in another way. Fastlane has many options and it’s impossible to write about all of them. For our app this solution was enough, so it will probably be okay in most cases. Feel free to try it out and customize it for your needs. Fastlane documentation is great for getting in-depth information. If you have any questions just ask us on our chat, I will be happy to answer.

There is still some place for improvement - for example adding Github actions. As I said before, maybe someday I will write another article or extend this one. And maybe your messages will speed up this process. :-)

Written by Mateusz Bętka.