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:
- Provide the package name for the application when asked (e.g. com.my.app), which can be found in your AndroidManifest.
- Press enter when asked for the path to your JSON secret file, we will deal with that right after.
- 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:
- android.injected.signing.store.file: Path to the release.keystore file.
- android.injected.signing.store.password: Keystore password.
- android.injected.signing.key.alias: Key alias of the release.keystore file.
- 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