In this article, we will guide you toward a solid automatic Android deployment solution leveraging React Native, Fastlane, and CircleCI. React Native offers an intuitive and responsive user interface and allows us to create apps for multiple platforms from the same code base. Fastlane provides developers with a full set of tools and actions to speed up and automate the mobile release process. Finally, CircleCI completes the solution by seamlessly adding continuous integration into the solution, making the build process simple, automatic, and repeatable.
There are a number of prerequisite steps that must be completed before we can officially begin setting up our solution. Before going any further we need:
play-store-credentials.json
. To obtain this file, follow these steps.Getting started with Fastlane is pretty simple. In your android folder, use bash to initiate Fastlane with the following line of code.
cd android Fastlane init
Next, follow these steps:
1. The package-name
can be found in the android/app/build.gradle file as applicationId
:
Note: If you are using a different applicationId, depending on the flavor of Android being built, you can just set the default one at this point. We’ll cover how to submit multiple flavors later in this article.
2. For the step asking the ‘Path to the JSON secret file
’, paste the following: ./Fastlane/playstore-credentials.json
.
3. When asked to ‘Download existing metadata and setup metadata management?
’, type ‘N
’, unless you want to download the metadata. We will not cover that topic in this post, but if downloading the metadata, be sure to remember to add it to the .gitignore file. See this example for more information. Once finished, add your `play-store-credentials.json
` to the Fastlane folder that was just created. Then, add this file to the .gitignore file, as we don’t want to have this in our repo.
android/fastlane/play-store-credentials.json
Next, we are going to set up two lanes
for automated deployment, one for staging, and another for production. A lane
is a group of instructions to be executed in a particular order, similar to a function in JavaScript. Feel free to add as many lanes as desired for your deployment. The process is largely the same for each lane.
Go to your Fastfile (fastlane/Fastfile) and remove all the content inside platform :android do
, the remaining file should look like this:
default_platform(:android)
platform :android do
end
Now, let’s create some lanes! All of them should be created within platform :android do
and the end
statement.
First, we’re going to create a lane called ‘Staging’. It is always best practice to provide a description before the lane definition.
desc "Submit a new Staging Build to Google Play Internal Track"
lane :staging do
end
Inside this staging, add the ‘gradle
’ instructions to create the build.
Gradle
requires, which can bundle
or assemble
. We are going to choose the bundle
option. The flavor for our example will be, and the build type will be Debug
or Release
. Since we are planning to submit to the store, it makes sense to create a Release
.
gradle(
task: "bundle",
flavor: "Staging",
build_type: "Release"
)
In the next line, add the `upload_to_play_store`
command to submit the build to Google Play Store.
Upload_to_play_store
requires a track
, which can be any of the tracks available on the Google Play Store. For the sake of simplicity, we are using the internal
track to submit staging builds. Package_name
is optional if you are not using flavors.
upload_to_play_store(
track: 'internal',
package_name: 'com.flatirons.androidStaging'
)
If you want to run this command on your machine, it is recommended to do a clean before generating a new build, so add this at the beginning of the staging lane.
gradle(task: "clean bundleStagingRelease")
When done, your Fastfile should look something like this:
default_platform(:android)
platform :android do
desc "Submit a new Staging Build to Google Play Internal Track"
lane :staging do
gradle(task: "clean bundleStagingRelease")
gradle(
task: "bundle",
flavor: "Staging",
build_type: "Release"
)
upload_to_play_store(
track: 'internal',
package_name: 'com.flatirons.androidStaging'
)
end
end
Creating a lane is largely the same process, regardless of function or purpose. To create a ‘production’ lane, only a few changes need to be made. All you have to do is change the lane name to :production
and thepackage_name
if you are using flavors.
To introduce continuous integration into our solution, we are going to leverage CircleCI, and more specifically, CircleCI Orbs to improve our deployment solution. An orb is a reusable snippet of code that helps to automate repeated processes, accelerate project setup, and integrate 3rd party tools. CircleCI provides an open registry of published Orbs, making it easy to find and leverage an Orb that suits your needs.
For our deployment, we will be leveraging the react-native-community’s orb, allowing us to outsource the environment configuration and maintenance. This orb also includes tools for testing, but we will not be using those for this deployment. If interested in end-to-end testing, see this post for more info.
First, create a CircleCI Config.yaml file.
touch .circleci/config.yml
Add the version, orbs dependencies, then create 3 keywords in that file. Indentation is very important in this file.
yml
version: 2.1
orbs:
rn: react-native-community/react-native@7.1.1
commands:
jobs:
workflows:
To verify you have a valid config.yml file, you can run the circleci orb validate .circleci/config.yml
command in your root folder.
In the orbs
section, we are defining a dependency named ‘rn
’, (this alias is used to reference all the predefined tasks in this orb but can be changed to anything you want).
The commands
section is a place to define reusable instructions, typically used by the jobs
.
Jobs
are detailed sets of instructions to be run in workflows.
Workflow
is a place where we can set rules and the order in which jobs are run, for example, ensuring a job is run on a specific branch, or creating dependencies between jobs (such as not running fastlane deployments if tests are failing).
With the above in mind, let’s add some useful commands to our commands
section.
yml
commands:
install_packages:
steps:
- restore_cache:
key: yarn-v1-{{ checksum "yarn.lock" }}-{{ arch }}
- restore_cache:
key: node-v1-{{ checksum "package.json" }}-{{ arch }}
- run: yarn install
# Uncomment the following line if you need to patch your packages
# - run: yarn patch-package
- save_cache:
key: yarn-v1-{{ checksum "yarn.lock" }}-{{ arch }}
paths:
- ~/.cache/yarn
- save_cache:
key: node-v1-{{ checksum "package.json" }}-{{ arch }}
paths:
- node_modules
The above install_packages
command is used to do a yarn install, and any other operations related to the node_modules
.
create_android_key_files:
steps:
- checkout
- run:
name: Decrypting Key Store
command: |
echo $ANDROID_ENCRYPTED_KEY_STORE | base64 -d > ./android/app/release.keystore
- run:
name: Decrypting Google Play Upload Key
command: |
echo $GOOGLE_PLAY_SERVICES_UPLOAD_KEY | base64 -d > ./android/fastlane/play-store-credentials.json
- persist_to_workspace:
paths: .
root: .
Create_android_key_files
will be used to create the release.keystore
and the play-store-credentials.json
mentioned earlier in the article.
create_env_file:
parameters:
api_url:
type: string
steps:
- checkout
- run:
name: Create Env File
command: |
echo 'Creating .env file!'
touch .env
echo "API_URL=<>">>.env
- persist_to_workspace:
paths: .
root: .
The create_env_file will be used to generate the .env file, you can ignore this if you are not using env config.
install_fastlane:
steps:
- checkout
- run:
command: gem install bundler
name: Install bundler
- run:
command: gem install fastlane
name: Install Fastlane
- persist_to_workspace:
paths: .
root: .
Finally, we have the install_fastlane
command, which provides instructions to install the latest fastlane version.
Now, we are ready to use these commands to create the jobs:
yml
jobs:
# Env configurations
checkout_code:
working_directory: ~/repo
environment:
TZ: "America/New_York"
docker:
- image: circleci/node:latest
steps:
- checkout
- persist_to_workspace:
paths: .
root: .
Checkout_code
uses CircleCI’s checkout command.
create_staging_env_file:
working_directory: ~/repo
docker:
- image: circleci/node:latest
steps:
- create_env_file:
api_url: $API_URL_STAGING
Create_staging_env_file
uses the code>create_env_file command to generate the .env file with staging-specific environment variables.
create_production_env_file:
working_directory: ~/repo
docker:
- image: circleci/node:latest
steps:
- create_env_file:
api_url: $API_URL_PRODUCTION
Create_production_env_file
uses the create_env_file
command to generate the .env file with production-specific environment variables.
# Testing
code_testing:
working_directory: ~/repo
executor:
name: rn/linux_js
node_version: "16.5"
steps:
- attach_workspace:
at: .
- install_packages
- run:
command: yarn test
name: Run Tests
# Add more commands if you need to test lint or typescript
As you might guess from the name, code_testing
runs tests on the app.
# Deployment
fastlane_android_release:
working_directory: ~/repo
executor:
name: rn/linux_android
gradle_options: "-Xmx4608m -XX:+UnlockExperimentalVMOptions -XX:+UseContainerSupport -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8"
java_options: "-Xmx4608m -XX:+UnlockExperimentalVMOptions -XX:+UseContainerSupport -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8"
resource_class: large
parameters:
lane:
type: string
label:
type: string
steps:
- attach_workspace:
at: .
- install_packages
- install_fastlane
- create_android_key_files
- run:
command: cd android && fastlane << parameters.lane >>
name: Upload << parameters.label >> build to Google Play
Fastlane_android_release
installs fastlane, and runs a specific lane. Be aware that resource_class: large
is being used in this example. If your build can use a less demanding resource class, go for it. Learn more about resource_classes
here. The gradle_options
and java_options
defined in this job are there to allow the build creation to use more ram and avoid errors when generating the build.
Once these jobs are configured, all we need to do to complete our config.yml file is to define the workflow.
yml
workflows:
flatirons_example:
jobs:
- checkout_code
- code_testing:
name: "Run tests"
requires:
- checkout_code
- create_staging_env_file:
name: "Create Staging .env"
filters:
branches:
only:
- staging
requires:
- code_testing
- create_production_env_file:
name: "Create production .env"
filters:
branches:
only:
- main
requires:
- code_testing
###############
### Staging ###
###############
- fastlane_android_release:
name: "Upload android staging build"
lane: staging
label: Staging
filters:
branches:
only:
- staging
requires:
- create_staging_env_file
################
## Production ##
################
- fastlane_android_release:
name: "Upload android production build"
lane: production
label: Production
filters:
branches:
only:
- main
requires:
- create_production_env_file
In this example, we’ve created a flatirons_example
workflow, where code_testing
needs to be successful in order to create the .env files that will later be used by the fastlane command when creating the build.
Using the filters
property, you can assign jobs to run on only specific branches.
The requires
parameter allows us to avoid running jobs if another one fails.
Additionally, with the name
key, more legible names can be displayed in the CircleCI dashboard.
As the last stage of our deployment, we will be configuring CircleCI, along with all the necessary environment variables.
First, let’s go ahead and configure CircleCI by following these instructions.
As we have already created our .circleci/config.yml in the previous steps, we will now select the ‘Fastest: Use the .circleci/config.yml in my repo
’ option.
In this example, we have two environment variables that will be used in the app (API_URL_STAGING
, and API_URL_PRODUCTION
), and others used by the config.yml (ANDROID_ENCRYPTED_KEY_STORE
, and GOOGLE_PLAY_SERVICES_UPLOAD_KEY
).
API_URL_STAGING
and API_URL_PRODUCTION
act as the URL for your API depending on the environment. To set these environment variables in CircleCI follow these instructions.
Before we do the same with ANDROID_ENCRYPTED_KEY_STORE
and GOOGLE_PLAY_SERVICES_UPLOAD_KEY
, we need to obtain the values for them.
To obtain these values, we are going to do a simple base64 encryption to get the file content as a string. Let’s start with the android release.keystore. Run the following command:
cat ./android/path/to/release.keystore | base64 > encrypted.txt
Copy the content of this file, then add it as the ANDROID_ENCRYPTED_KEY_STORE
environment variable in CircleCI, and delete the file.
Now, do the same with your play-store-credentials.json
file.
cat ./android/Fastlane/play-store-credentials.json | base64 > encrypted.txt
Copy the content of this file, add it as the GOOGLE_PLAY_SERVICES_UPLOAD_KEY
environment variable in CircleCI, and delete the file.
If you want to verify that the encrypted string is the same as within the file, you can decrypt the string, and check that files are the same by using this command:
echo "encrypted. Text Content" | base64 -d > output.txt
cmp --silent ./android/path/to/file output.txt || echo "Files do not match"
If “Files do not match
” is printed within the console, it means the string content was not correctly input. If nothing is printed, then the string content has been validated.
At this point, you should be able to do an automatic release when merging changes into the staging branch. But there is a major issue, all builds are being submitted with the same versionCode
, this can generate conflict and in the majority of cases, the build will not be submitted.
To prevent this issue, in the android/app/build.gradle
above the android section, define this function:
static def getBuildVersion() {
def date = new Date()
def formattedDate = (int)(date.getTime() / 1000);
println("VersionCode: " + formattedDate)
return formattedDate
}
Set your versionCode
to use this function:
versionCode getBuildVersion() as int
And our deployment solution is good to go!
As a bonus, let’s show you how to have this environment set to send a slack message when the build is successfully created. By adding slack
after upload_to_play_store
, and running the following command, a notification will be pushed to Slack upon completion of any build:
slack(
message: "[ANDROID][Flatirons Example]: New staging build available on Google Play Internal Track [STAGING]",
slack_url: "https://api.slack.com/messaging/webhooks",
success: true,
payload: {}
)
And that’s it, you’ve just configured Fastlane and CircleCI, and can now sit down, relax, drink some coffee, read a book, or whatever floats your boat, until notified by Slack that your build has been completed!
Here you can check a complete guide to React Native and for working with React Native developers.