Go back blog

Fastlane Android Mobile App Deployment

Learn how to speed up the Android app deployment process with Fastlane. Discover tips and best practices for simplifying and automating your mobile app release process.

What is Fastlane?

Fastlane is an open-source platform that aims to simplify the daily bases of mobile programmers by offering us new ways of making Android and iOS deployment less painful. Essentially it lets you automate every aspect of your development and release workflow and spares you hours of work, either bumping versions or uploading your applications to stores.

Fastlane advantages

Some of Fastlane's functionalities and advantages include:

  • Automatically generate the needed screenshots for you to upload your apps to the stores.
  • Easily distribute beta builds for testers and automatically submit new versions of your app for review.
  • Publish new releases to the app store in seconds and with minimum effort.
  • Automating the most time-consuming beta distribution steps including incrementing the build version, code signing, building and uploading the app, and setting a changelog.
  • Support for over 15 beta testing services including TestFlight, Crashlytics Beta, Play, and Hockey.
  • Freely switch between beta services without needing to reconfigure Fastlane.
  • Create a repeatable custom workflow to build, upload and distribute new releases to the app store with things like GitHub actions or GitLab ci/cd.
  • Deploy from any computer.

Implementing android app deployment in a react native project using Fastlane and GitHub

So now that we've seen what Fastlane is and what it does, let us skip to the most important part, how to use it. And in this case, we will implement it with the support of GitHub actions!

Create a Google Developers service account

First, to be able to deploy to the store, we need to have a "Google Developers service account", if you don't have one, create one. Then export the credentials so we can be able to automate the process.

  • Open the Google Play Console.
  • Access Account Details and get your Developer Account ID.
  • Go to the Google Cloud Platform and create a new service account.
  • Name your service account and check if your "Developer Account ID" matches the one on the light gray text in the second input, preceding .iam.gserviceaccount.com. If not, open the picker in the top navigation bar, and find the one with the ID that contains it, the description is optional.
  • Select a role, then find and select Service Account User (under service accounts), and proceed.
  • Under "Service Accounts" click on the actions vertical three-dot icon of the service account you just created and select manage keys on the menu.
  • Create a new key (make sure JSON is selected as the type), and once you’re done with it a file will automatically be downloaded for further use in your project.
  • Return to the Google Play Console tab and click on "Manage Play Console permissions" on the new service account (refresh the page if it's not showing up).
  • Choose the permissions you'd like the account to have, for this tutorial I recommend the Admin (all permissions), but you should manually select the appropriate checkboxes for your project, you may leave out some of the Releases permissions, such as "Release to production" for example. At the end click “Invite user” to finish.

Setting up Fastlane

Firstly we will have Fastlane installed on our machine. Since I'm using macOS, I will be using: 

brew install fastlane

For "macOS/Linux/Windows" you should be using the following command, but check the Fastlane docs (https://docs.fastlane.tools) for more info on how to install.

sudo gem install Fastlane

With this, we will navigate to our android project and initialise Fastlane.

cd android
fastlane init

A prompt will show up where you need to follow these steps:

  1. Provide the package name for the application when asked (e.g. com.my.app), which can be found in your AndroidManifest.
  2. Press enter when asked for the path to your JSON secret file, we will deal with that right after.
  3. Answer 'n' (no) when asked if you plan on uploading info to Google Play via Fastlane (we will also set this up later).

Once it's finished, you should have a `Fastlane` folder within the android one (android -> Fastlane), after that, you will proceed to include the service account file you created in the last step (you may want to rename it). And then change the "Appfile" json_key_file to the service account file path.

In addition, under the Fastlane folder, you can validate your service account file by running the following command.

fastlane run validate_play_store_json_key json_key:your/path/service_account.json

We will also need to generate a Keystore file in order to sign in the app (if you use Android App Bundles, you need to sign the app bundle before you upload it to the Play Console), and for that, we will use the keytool command.

keytool -genkey -v -keystore release.keystore -alias <your key alias> -keyalg RSA -keysize 2048 -validity 10000

A prompt will show up where you need to set the keystore password. There will be a set of questions that you should fulfil accordingly to your project, for this tutorial I will just leave them empty. Answer 'y' (yes) at the end.

Setting up the necessary plugins

We will also need to include two plugins that will help us:

  • Retrieving the version number of the application from the package.json file, which will define the version number of the Android App Bundle.
fastlane add_plugin load_json
> Should fastlane modify the Gemfile at path '/xxx/react-native-app/android/Gemfile' for you? (y/n) 
> y (answer yes here)

  • Setting the version code of your app.
fastlane add_plugin versioning_android

With this, a Pluginfile will be created under Fastlane. Further on, if you face any problems regarding detecting the plugins you may want to add this line as well.

plugins_path = File.join(File.dirname(__FILE__),'fastlane','Pluginfile')

Setting up the Fastfile

Now that we have Fastlane ready we will proceed to write some code to automatically build and deploy our application to the stores. And for starters, we will set our default platform (android in this case) and write a function that generates a unique versionCode for the Android App Bundle.

Instead of managing the version code manually it is based on a timestamp in seconds where any build done more recently is considered to be a higher version, and the versionCode is increase every minute (so max 1 build per minute) and cannot be smaller than the legacyVersionCode.

default_platform(:android)

def getVersionCode
  thirtySeptemberTwentyTwenty = 1601480940 / 60
  legacyVersionCode = 10902
  versionCode = legacyVersionCode + 
  (Time.now.to_i / 60) - thirtySeptemberTwentyTwenty

  if versionCode > 2100000000
    raise "versionCode cannot be higher than 2100000000"
  end

  versionCode.floor()
end

Fastlane works with "lanes", where each lane will have a set of actions to execute, and our first lane (bump_build_number) will be accountable for using our last function to update android.defaultConfig.versionCode in the app/build.gradle.

platform :android do   
    desc "Increments internal build number tracking" 
        lane :bump_build_number do 
            android_set_version_code( 
            version_code: getVersionCode() 
        ) 
    end 
...

With that said and done, we will start to build our playstoreInternal lane by retrieving the application version from the package.json file (in the react native case).

...
    desc "Build and upload the app to playstore"
        lane :playstoreInternal do |options|
        package = load\_json(json\_path: "../package.json")
...

Then we clean up the build folder and use the bump_build_number lane created above.

...
    gradle(
      task: "clean"
    )

    bump_build_number
...

Finally, we define a gradle task to build the application in the app/build.gradle file.

...
    gradle(
      task: 'bundle',
      build_type: 'Release',
      properties: {
        "android.injected.signing.store.file" => Dir.pwd +                        "/release.keystore",
        "android.injected.signing.store.password" => options[:RELEASE_KEYSTORE_PASSWORD],
        "android.injected.signing.key.alias" => options[:RELEASE_KEYSTORE_ALIAS],
        "android.injected.signing.key.password" => options[:RELEASE_KEYSTORE_KEY_PASSWORD],
        "vname" => package["version"]
      }
    )
...

Four different properties are needed to allow gradle to sign our application:

  1. android.injected.signing.store.file: Path to the release.keystore file.
  2. android.injected.signing.store.password: Keystore password.
  3. android.injected.signing.key.alias: Key alias of the release.keystore file.
  4. android.injected.signing.key.password: Key password.

To check if everything is running properly so far you can then execute the following command:

fastlane android playstoreInternal RELEASE_KEYSTORE_PASSWORD:keystorePassword RELEASE_KEYSTORE_ALIAS:keyAlias RELEASE_KEYSTORE_KEY_PASSWORD:keyPassword

If everything checks up you should get something like this:

Later on, we will add this line for store deployment.

...
    upload_to_play_store
  end
end

In the end, your file will look something like this:

default_platform(:android)

def getVersionCode
  thirtySeptemberTwentyTwenty = 1601480940 / 60
  legacyVersionCode = 10902
  versionCode = legacyVersionCode + (Time.now.to_i / 60) - thirtySeptemberTwentyTwenty

  if versionCode > 2100000000
    raise "versionCode cannot be higher than 2100000000"
  end

  versionCode.floor()
end


platform :android do   
  desc "Increments internal build number tracking" 
    lane :bump_build_number do 
      android_set_version_code( 
      version_code: getVersionCode() 
     ) 
  end 

  desc "Build and uploads the app to playstore"
    lane :playstoreInternal do |options|
    package = load_json(json_path: "../package.json")

    gradle(
      task: "clean"
    )

    bump_build_number

    gradle(
      task: 'bundle',
      build_type: 'Release',
      properties: {
        "android.injected.signing.store.file" => Dir.pwd + "/release.keystore",
        "android.injected.signing.store.password" => options[:RELEASE_KEYSTORE_PASSWORD],
        "android.injected.signing.key.alias" => options[:RELEASE_KEYSTORE_ALIAS],
        "android.injected.signing.key.password" => options[:RELEASE_KEYSTORE_KEY_PASSWORD],
        "vname" => package["version"]
      }
    )

    upload_to_play_store
  end
end

Setting up the GitHub actions

For the GitHub actions, we will start by creating a folder named .github, and inside that folder another one called workflows, where you will create your yml file that will hold the actions.

First, we start by naming the flow and setting when it will run, in this case, it will run every time we make a pull request regarding the branches developer and master.

name: Deploy

on:
  pull_request:
    branches:
      - development
      - master

After this we will set our jobs, each job has a maximum run time, where it will run, and all the dependencies needed to execute our Fastlane lane within GitHub. Each step will have a name, a docker image (uses), and might have some extra specifications (with), some may have to run some extra commands to set them up correctly.

jobs:
  build:
    timeout-minutes: 20
    runs-on: ubuntu-latest 

    steps:
      - name: Checkout code 
        uses: actions/checkout@v2

      - name: Setup node
        uses: actions/setup-node@v3
        with:
          node-version: 16

      - name: Setup Ruby 2.7.4
        uses: ruby/setup-ruby@v1
        with:
          ruby-version: '2.7.4'
          bundler-cache: false
        env:
          ImageOS: macos1015

      - name: Setup JDK 11
        uses: actions/setup-java@v2
        with:
          java-version: 11
          distribution: adopt
      
      - name: Setup Android SDK
        uses: amyu/setup-android@v1.1

      - name: Install packages 
        run: | 
          yarn
      
      - name: Setup module dependencies cache
        uses: actions/cache@v2
        with:
          path: node_modules
          key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }}

      - name: Install module dependencies
        run:  yarn install --frozen-lockfile

      - name: Install Fastlane
        run: gem install fastlane
      ...

In this case, we will need:

  • Actions dependencies.
  • Node setup.
  • Ruby setup.
  • JDK and Android SDK setup.
  • Package installation.
  • Module dependencies cache setup.
  • Fastlane installation.

And the lane execution that we already tested beforehand

      ...
      - name: Build and Publish Mobile App
        run: | 
          cd android
          fastlane android playstoreInternal
          RELEASE_KEYSTORE_PASSWORD:keystorePassword
          RELEASE_KEYSTORE_KEY_PASSWORD:keyPassword
          RELEASE_KEYSTORE_ALIAS:keystoreAlias 

Encrypting sensitive information

Now, something that we need to take into account is that both the release.keystore and the services account JSON will be pushed into git and contain sensitive information, so the best practise is to encrypt these, and for that, we will use GnuPG and GitHub secrets!

To start off we install GnuPG.

brew install gnupg

And then encrypt the files inside the Fastlane folder by running the following commands and entering a password for each.

sudo gpg --symmetric --cipher-algo AES256 release.keystore
sudo gpg --symmetric --cipher-algo AES256 service_account.json  

Once this is done two .gpg files will be added to your project.

To ensure that your shell script is executable before checking it into your repository also run the following commands:

sudo chmod +x service_account.json.gpg
sudo chmod +x release.keystore.gpg

In the end make sure you delete these files:

  • service_account.json
  • release.keystore

If you run into any problems regarding the GPG pinentry module be sure to follow these steps:

brew install pinentry
which pinentry

Then copy the path and change the one on the following file.

nano ~/.gnupg/gpg-agent.conf

Lastly, run the following command and you will be ready to go.

gpgconf --kill gpg-agent

Adding GitHub secrets

At GitHub, we will need to create 7 different secrets. For that go to Settings and then on the left side menu go to Security -> Secrets -> Actions.

For the files simply copy and paste the content, as for the rest, just type the passwords and the alias.

For the release.keystore file we will need:

  • Content of release.keystore.gpg.
  • Alias of the file.
  • The Key password of the file.
  • Passphrase for decrypting the file (GPG Password).
  • The password of the file.

And for the service_account.json we will need:

  • Content of service_account.json.gpg.
  • Passphrase for decrypting the file (GPG Password).

Adding the script to decrypt the variables

Now like we did for the GitHub actions we are going to create a folder named "scripts" inside the ".github" folder. This is where we will add our "decrypt_android_keys.sh" file.

And where we will include the necessary commands for GPG to decrypt the .gpg files.

gpg --quiet --batch --yes --decrypt --passphrase="$RELEASE_KEYSTORE_PASSPHRASE" \
--output android/fastlane/release.keystore android/fastlane/release.keystore.gpg

gpg --quiet --batch --yes --decrypt --passphrase="$SERVICE_ACCOUNT_PASSPHRASE" \
--output android/fastlane/service_account.json android/fastlane/service_account.json.gpg

After this, we will proceed to change our Github actions so they will work with the GitHub secrets to decrypt our files. We start by complementing the action dependencies with the necessary code to decrypt them, and to finish off, we replace the keys for the build and publishing.

...
- uses: actions/checkout@v2
- name: Checkout code and decrypt Android keys 
    run: sh ./.github/scripts/decrypt_android_keys.sh 
    env: 
        RELEASE_KEYSTORE: ${{ secrets.RELEASE_KEYSTORE }} 
        RELEASE_KEYSTORE_PASSPHRASE: 
        ${{ secrets.RELEASE_KEYSTORE_PASSPHRASE }} 
        SERVICE_ACCOUNT: ${{ secrets.SERVICE_ACCOUNT }} 
        SERVICE_ACCOUNT_PASSPHRASE: 
        ${{ secrets.SERVICE_ACCOUNT_PASSPHRASE }} 
...
 - name: Build and Publish Mobile App
    run: | 
        cd android
         fastlane android playstoreInternal
         RELEASE_KEYSTORE_PASSWORD:
         ${{secrets.RELEASE_KEYSTORE_PASSWORD}}
         RELEASE_KEYSTORE_KEY_PASSWORD:
         ${{secrets.RELEASE_KEYSTORE_KEY_PASSWORD}}
         RELEASE_KEYSTORE_ALIAS:
         ${{secrets.RELEASE_KEYSTORE_ALIAS}}

In conclusion

Fastlane is an excellent tool to help you deploy your apps quickly from anywhere and with minimum effort, once the initial configuration is done you don’t need to worry about maintenance, just do your changes and Fastlane will do the rest for you