Continuous Delivery - Integration for Everyone

Authors
Jakub Benes
Software Engineer
@
Callstack

You know what I’m talking about: you send PR into your favorite (or even your own; or ideally both at the same time) project. And after a few seconds, you can see a beautiful report of what have you done wrong.

screenshot presenting code review

Honestly, there are plenty of information out there on the internet about these topics. It’s not my goal to write it everything once more. Why so, right? Let’s take another approach. I’m going to point you in the right direction and at the end of the day, hopefully, you will be able to tame CI/CD as well.

Case study

I’m going to describe a little bit simplified version of a use-case of a real client of Callstack that I had the honor to work with. The project itself is React Native application for both Android and iOS. But a lot of the knowledge I’m going to share, you can easily use with web projects too. (It will be probably even easier to setup, so keep reading). The code was hosted on GitHub (feeling blessed once more!) so the choice of the proper CI service was obvious: CircleCI or Travis CI. Both are great services but I’ve picked CircleCI because I’m already familiar with the workflows I’ll describe later.

The next important step is to decide what we want to achieve in the first place. Our requirements were quite simple. We want to establish a simple (yet effective) workflow. Let’s have a master branch and from that branch, each programmer should spin off his functionality. Once all checks are successful we should be able to merge PR into master branch and then additional (deploy) jobs are going to be run.

master branch in react native

Yeah, a lot of you could say that GitFlow is more robust — but let’s start with a small, we can enhance it anytime. We want to get things done™ at the first place.

It should work exactly as I’ve illustrated on the diagram. I will create a PR with my awesome functionality and CircleCI should be able to check code for:

  • FlowType
  • ESlint issues
  • Basic unit tests powered by Jest

And eventually, run Danger.js. Danger.js? You know, sometimes you want to also check that PR creating new UI element contains image, that you haven’t left sloppy console.log and etc. — that’s a job for Danger.js, but let’s talk about this later.

OK, that would be enough. The next and the last yet important flow would be when the PR will be merged into the master branch. Then we would like to run another job on the CircleCI — deploy iOS and Android version to our alfa testers and inform everyone on the Slack channel that some sweet is coming directly into their devices. What? Wait a second. How?

Good question. We ended up with App Center service. It’s lovely service, it supports distribution to testers, crash logs, analytics and even more out of the box. The distribution is just simple as it can be and you can also send your app to the Stores directly from it. The next thing we need to think about is who is going to build our app for distribution. If you are using Metro bundler you are safe and you can completely rely on App Center because it works out of the box as CI. As we are using Haul — our homegrown Webpack-based builder for React Native — it would be better for us introduce our own building process. For that, we are going to use Fastlane to automate the mess around certificates, provisioning profiles and everything — especially on iOS side. So, what do you think? Sounds like a plan, doesn’t it? Let’s jump directly into it!

Circle CI configuration

The first what we have to do is set up a project on CircleCI, it should be easy. Go into Circle CI / Add Project and click on your lovely project. Now the fun comes into play. CircleCI is easily configurable via special YAML file living inside of your repository as .circleci/config.yml so let’s create one.

##
# Hello CircleCI! Version 1 is obsolete so...
version: 2
##
# Define our enviroment.
# This "defaults: &defaults" magic is known as Yaml anchors
# it allows us point to this config from several parts of the document. Neat!
#
defaults: &defaults
working_directory: ~/project
docker:
- image: circleci/node:latest
android_defaults: &android_defaults
working_directory: ~/project/android
docker:
- image: farwayer/react-native
environment:
# This configuration works best for our project, but you might want to experiment here a little
- LANG: "en_US.UTF-8"
- _JAVA_OPTIONS: "-Xms518m -Xmx2048m"
- GRADLE_OPTS: '-Dorg.gradle.jvmargs="-Xmx2048m -XX:+HeapDumpOnOutOfMemoryError"'
- BUILD_THREADS: 2
ios_defaults: &ios_defaults
working_directory: ~/project/ios
macos:
xcode: "9.1.0"
environment:
- LANG: "en_US.UTF-8"
# This is important for Fastlane because we create keychain base on this
- MATCH_KEYCHAIN_NAME: "circle"
- MATCH_KEYCHAIN_PASSWORD: "circle"
# To be continued...

As I mentioned before, if you are using Metro bundler you can ignore both android_defaults and ios_defaults — you can set the building (triggered with every merge to the master) easily in App Center UI. That’s not our case, though, because we rely on Haul. Once we have configured our defaults for the jobs (check out the terminology) we should configure the jobs as well.

# ...let's define our jobs
jobs:
# let's install our project's dependenices...
install-dependencies:
<<: *defaults
steps:
- checkout
- attach_workspace:
at: ~/project
# ...restore cache if there is any...
- restore_cache:
keys:
- v1-0-dependencies-{{ checksum "package.json" }}
# fallback to using the latest cache if no exact match is found
- v1-0-dependencies-
# ... of not run yarn...
- run:
name: Install Dependencies
command: yarn
# ... cache the dependencies...
- save_cache:
key: v1-0-dependencies-{{ checksum "package.json" }}
paths:
- node_modules
- yarn.lock
# ...and persist to the workplace, because we want to access this within the workflows (other jobs, etc.)
- persist_to_workspace:
root: .
paths: .
# Introduce Flow job – let's staticaly check our code...
flow:
<<: *defaults
steps:
- attach_workspace:
at: ~/project
- run: yarn flow
# Let's check the code style as well...
lint:
<<: *defaults
steps:
- attach_workspace:
at: ~/project
- run: yarn lint
# Hell yeah! WE HAVE PLENTY OF THEM...
unit-tests:
<<: *defaults
steps:
- attach_workspace:
at: ~/project
- restore_cache:
keys:
- v1-0-jest-cache-{{ checksum "package.json" }}
# fallback to using the latest cache if no exact match is found
- v1-0-jest-cache-
- run: yarn test:ci
- save_cache:
key: v1-0-jest-cache-{{ checksum "package.json" }}
paths:
- .jestCache
- persist_to_workspace:
root: .
paths: .
# iOS / Android jobs to by continued...
view raw gistfile1.txt hosted with ❤ by GitHub

This part should be quite self-describing. We have just defined the job for:

  • Installing dependencies (along with caching)
  • ESlint check
  • FlowType check
  • Jest — for unit testing (along with caching)

Now we have to define the job for iOS and Android deployment.

##
# This part is little bit tricky but still it's not a rocket sience...
#
ios-beta:
<<: *ios_defaults
steps:
# Attach to workplace (there are our saved node_modules)...
- attach_workspace:
at: ~/project
# Install CocoaPod for managing our iOS native dependencies...
- run:
name: Install CocoaPod Spec
command: curl https://cocoapods-specs.circleci.com/fetch-cocoapods-repo-from-s3.sh | bash -s cf
# Install Fastlane...
- run:
name: Install Ruby and Fastlane
command: |
brew install ruby
gem install fastlane
# Install our native dependencies...
- run:
name: Update CocaPods dependencies
command: pod install --verbose
timeout: 1200
# Update Fastlane (Fastane plugins, namely appcenter plugin)
- run:
name: Bundle update
command: bundle update
# Run our iOS deploy task
- run:
name: Fastlane
command: bundle exec fastlane beta
##
# Let's do almost the same for Android...
# It could be worth it to create custom tailored Docker image with Fastlane preinstalled but I wanted to be explicit
android-beta:
<<: *android_defaults
steps:
# Attach to workplace (there are our saved node_modules)...
- attach_workspace:
at: ~/darwin
# Optimize build.gradle because memory limitation on our CircleCI machine...
- run:
name: "Optimize build.gradle for CI"
command: sed -i 's/2g/1g/' app/build.gradle
# Install Fastlane
- run:
name: Install Fastlane
command: |
gem install fastlane -NV
# Update Fastlane
- run:
name: Bundle update
command: bundle update
# Run our Andorid deploy task
- run:
name: Fastlane
command: bundle exec fastlane beta
# to by continued...
view raw gistfile1.txt hosted with ❤ by GitHub

As you can see in those jobs we rely on Fastlane tool. We are going to dive more into it the next chapter. Let’s wrap it everything together and create a workflow for that two paths: when someone creates PR and when the PR is merged.

# ...let's create our workflows
workflows:
# yeah we use the latest version of CircleCI
version: 2
# Run under every PR this...
test:
# ...jobs...
jobs:
# At first, install dependencies...
- install-dependencies
# If it succeeded...
# Run ESlint...
- lint:
requires:
- install-dependencies
# Run FlowType check...
- flow:
requires:
- install-dependencies
# Run Jest unit tests...
- unit-tests:
requires:
- install-dependencies
# If unit tests passed and we are on MASTER branch...
# Deploy iOS...
- ios-beta:
requires:
- install-dependencies
- unit-tests
filters:
branches:
only: master
# Deploy Android...
- android-beta:
requires:
- install-dependencies
- unit-tests
filters:
branches:
only: master
view raw gistfile1.txt hosted with ❤ by GitHub

It should look like this. I’ve tried to place comments here and there so I hope you can fight it through. The only question is what’s the magic and mysterious Fastlane, we will get to it in the next chapter. Here is a full configuration what we’ve used so far. Now brace yourself we are going to dive in into the best tool what happened to mobile developers since… the creation of the universe probably. The mighty Fastlane.

Fastlane

If you don’t know what’s Fastlane, I will try to briefly describe it. Basically, without any unnecessary buzzword, it’s a deployment automation tool which does the job just right. You probably know how painful is to deal with certificates and provisioning profiles on iOS, so we’ll use Fastlane to simplify this for us. I mean, certificates and provisioning profiles are not that hard — updating them and sharing across the team is. That’s the time when matchsteps in.

We are going to build an application, send binary to App Center and notify our Slack channel that another version of our awesome app is on its way. Fastlane created concept of lanes which is basically in common language a fancy way how Fastlane calls its plugins. There are many of them preinstalled so you can use it right away. For us, it will be Match on iOS side and Gradle for Android, but it’s not everything at all. There are actions for taking screenshots, running tests, deploying to TestFlight, and many more, check out the list by yourself on Fastlane docs. We want to keep our example configuration simple so we will be good just with match+gym, gradleandslack. We all will use 3rd-party plugin for App Center called simply appcenter so you have to also obtain an API token for it.

To proceed we need to install Fastlane. It should be pretty straightforward as brew cask install fastlane. Once you will be done go to ios and androidfolder of your project and at both places run fastlane init

  • For iOS choose 4) Manual setup option, the rest of it I’ll cover in following paragraphs so stay with me. 💪
  • For Android, you will be asked for package name so don’t forget to type your correct bundle id. Question regarding path to json secret you can ignore it, we don’t need it for our case.

The last thing that you need to do is run fastlane add_plugin appcenter in both directories as well. Now we should be ready for actual configuration.

iOS

The iOS is, as I said above, a little bit trickier regarding the signing of binary. However match will solve the problem for us. Match actually sync our certificates and provision profiles using git. Thus your whole team could access correct configuration and even more. Match can create new provision profiles or certificates that you require for specific build. This is especially convenient for CI because you don’t need to upload your certificates to it — everything is stored in git hashed by passphrase that you choose at the beginning.

Fastlane operates based on two files: Appfile and Fastfile. Before we proceed to configure Match we should update our Appfile as well.

app_identifier [""] # The bundle identifier of your app
apple_id "" # Your Apple email address
team_id "" # Developer Portal Team ID
view raw gistfile1.txt hosted with ❤ by GitHub

If you are done with Appfile editing, we can start with Match. All you have to do is to follow the official instruction:

  • Create private repository
  • Run fastlane match init

You will be asked for a password to the user you’ve just configured as apple_id. If everything went OK you can test your very new configuration by running fastlane match adhoc, where adhoc means we want to use ad-hoc certificate, the one we need for App Center.

If you are positive about it — we have big part of our quest behind us. Good.

Now go ahed and open fastlane/Fastlane. We want to achieve configuration similar to this.

fastlane_version "2.89.0"
default_platform :ios
platform :ios do
lane :beta do
if is_ci?
create_keychain(
name: ENV["MATCH_KEYCHAIN_NAME"],
password: ENV["MATCH_KEYCHAIN_PASSWORD"],
timeout: 1200
)
end
match(type: "adhoc")
gym(
scheme: "darwin",
silent: true,
clean: true,
sdk: "iphoneos11.1",
# this was specific for our project (for our native dependency) so you can omit this line
# but still I could be good to know
export_options: { compileBitcode: false },
export_method: "ad-hoc"
)
appcenter_upload(
api_token: "YOUR_API_TOKEN",
owner_name: "YOUR_OWNER_NAME",
group: "YOUR_GROUP_NAME",
app_name: "YOUR_APP_NAME",
ipa: lane_context[SharedValues::IPA_OUTPUT_PATH],
dsym: lane_context[SharedValues::DSYM_OUTPUT_PATH],
release_notes: "This release is **automatic** from *master* branch.\n\n#{last_git_commit[:message]}",
)
clean_build_artifacts
id = lane_context[SharedValues::APPCENTER_BUILD_INFORMATION]['id']
slack(
username: "",
icon_url: "",
message: "<https://install.appcenter.ms/users//apps//releases/#{id}/|iOS Releases>\n🚀 Release number: #{id}",
)
end
error do |lane, exception|
slack(
username: "",
icon_url: "",
message: exception.message,
success: false
)
end
end
view raw gistfile1.txt hosted with ❤ by GitHub

The commands are (I hope) self-describing and you call always look them up on Fastlane website. However, we need to focus on some additional setting on Circle CI side, namely, we need to set few env variables.

  • MATCH_PASSWORD should be set to match your passphrase what you have set during the Match initialization
  • SLACK_URL what should be Slack’s incoming web-hook URL.
  • FASTLANE_PASSWORD password to theapple_id user

If you did so, you should be all set. Now it’s time to test it. Navigate intoiosdir of your project and run fastlane beta and let’s see what happens. If everything went OK you should be able to see a success message on your Slack channel. If you do not, and I really hope that this wouldn’t be your case, write me in the comments, I’ve might be able to help you.

Android

Actually, to make our build pass in App Center, we need to sign it as well. Fortunately for us, it’s much easier and we can persist key inside of our repo. Signing is quite well described on Android website. Open your project in Android Studio and navigate to File > Project Structure.

screenshot presenting android project structure

Probably, the only thing you have to do is to create the keychain for this operation. As it suggested in previously linked documentation is easy as:

keytool -genkey -v -keystore my-release-key.jks -keyalg RSA -keysize 2048 -validity 10000 -alias my-alias
view raw gistfile1.txt hosted with ❤ by GitHub

Then you just fill those fields —and that’s it, your app will be signed from that moment. Now it’s time for Fastlane for the Android side of your project. Honestly, and that’s a good thing, it will be quite similar.

fastlane_version "2.89.1"
default_platform :android
platform :android do
lane :beta do
gradle(task: "assembleRelease")
appcenter_upload(
api_token: "YOUR_API_TOKEN",
owner_name: "YOUR_OWNER_NAME",
group: "YOUR_GROUP_NAME",
app_name: "YOUR_APP_NAME",
apk: lane_context[SharedValues::GRADLE_APK_OUTPUT_PATH],
release_notes: "This release is **automatic** from *master* branch.\n\n#{last_git_commit[:message]}",
)
clean_build_artifacts
end
after_all do |lane|
id = lane_context[SharedValues::APPCENTER_BUILD_INFORMATION]['id']
slack(
username: "",
icon_url: "",
message: "<https://install.appcenter.ms/users//apps//releases/#{id}/|Android Releases>\n🚀 Release number: #{id}",
)
end
error do |lane, exception|
slack(
username: "",
icon_url: "",
message: exception.message,
success: false
)
end
end
view raw gistfile1.txt hosted with ❤ by GitHub

Gradle lane will generate our signed APK so we can forward it directly to App Center. Easy as that!

It’s time to test! Let’s create a dummy PR inside your project and you should see you pipeline spinning! If everything is green and beautiful, don’t hesitate to merge it and immediately jump into CircleCI Dashboard to watch our goodness. Is’s very likely that something may go wrong. Don’t be scared, the easiest thing you can do is to debug the pipeline directly in CircleCI container. You can always click on “Rerun job with SSH” to get directly into that container through SSH — there you can easily investigate what went wrong. Fastlane could be also quite tricky, but I’ve found out that searching through their bug tracker works usually good.

It’ll take some time to properly hack those thing together, but trust me, once you do it your developer experience will be suddenly awesome and you won’t be willing to go back.

If you have still some energy to spend and especially if you are working in a big team. I would suggest you to check out Danger.js that I have mentioned at the begining of this article. You can, for instance, see how Spectrum is using it. It’s a great tool to enforce some practise across the whole team. Everything that could be checked by the machine should be checked by the machine. Have you introduced a new component? Let’s attach an image to PR of it! Have you introduced a new file, does it have @flow annotation? It’s just about your imagination because Danger.js how its name suggests is written in JavaScript. Thus it’s very easy (and convenient) for us to script it — the API is more then friendly.

So, be honest, did you manage to run it? In both cases, we would always appreciate if you share your thoughts with us. I would love to help you if you run into some problem as well, so do not hesitate.

Insights

Learn more about CI/CD

cover

Aug 21Article

Automating React Native Evals With Airflow

How Airflow coordinates React Native Evals, runs solver and judge models separately, and keeps every result traceable through human review.

cover

Jun 19Article

Caching React Native Builds on S3 and R2 With Rock

Learn how to cache your React Native builds and store them on Amazon S3 or R2 to reuse across your team.

cover

Oct 29Podcast

How to Handle App Center Retirement

Discover App Center alternatives for React Native. Explore migration tips and Expo's role in the transition from CodePush & other App Center functionalities.

cover

Aug 4Article

Mastering Fastlane: Overcoming Challenges in Passing Build Properties to Native Layers

Master passing build properties with Fastlane for efficient application development.

cover

May 14Article

Ship OTA (Over-The-Air) When in an Emergency

Shipping Over-The-Air may save your app from a lot of trouble. Find out why and how you can make your application OTA-ready.

cover

May 10Article

Shipping Fast With Continuous Deployment

Continuous Deployment (CD) lets you ship new features or check for critical bugs fast. Find out more about developing apps with a CD setup.

cover

Dec 13Article

How Can Continuous Integration (CI) Improve Your React Native Apps?

Read the article and find out how Continuous Integration (CI) can improve the building, testing, and distribution of your React Native apps.

cover

Mar 19Tutorial

The Hard Way vs. The React Native AI SDK Way: Stop Writing Custom Modules for Every Model

Learn how React Native AI uses a unified provider model to switch between cloud and on-device LLMs without rewriting application logic, enabling reliable AI experiences online and offline.

cover

Mar 12Tutorial

Build Smarter Apps: Tool Calling & AI Orchestration Explained

Learn how on-device models in React Native can call tools, interact with external APIs, and return structured outputs you can use directly in application logic.

cover

Mar 5Tutorial

MLC LLM + React Native: On-Device AI Without the Pain

Learn how to run third-party on-device LLMs in React Native using MLC LLM. Choose your models, run them efficiently across platforms, and keep a unified JavaScript API with React Native AI.

cover

Feb 24Tutorial

Intelligent Fallbacks with Apple Intelligence in React Native

Learn how to use Apple’s built-in Foundation Models in React Native apps, reduce memory usage, and enable fast, fully on-device AI with React Native AI Apple.

cover

Feb 10Tutorial

What Is the React Native AI SDK? A Complete Intro & Quickstart

Learn why on-device AI matters for React Native apps, how local LLMs behave offline, and what problems React Native AI is designed to solve from day one.

cover

Jan 29Tutorial

Implementing an Android TurboModule with Kotlin

Learn how to implement a React Native TurboModule on Android, from the TypeScript spec and Codegen to the Kotlin implementation and package registration.

cover

Jan 19Tutorial

Working with Different Threads in Swift TurboModules

Learn how JavaScript, native module, background, and main threads interact in React Native TurboModules, and how to use GCD queues to run async work safely in Swift.

cover

Jan 9Tutorial

How to Add Type-Safe Constants to Swift TurboModules

Learn how to expose typed, immutable constants from a Swift TurboModule using Codegen, and get full end-to-end type safety between native and JavaScript.

cover

Dec 30Tutorial

Adding Event Emitters to Your TurboModule in Swift

Learn how to emit native events from a Swift TurboModule and subscribe to them in JavaScript, including proper memory management and Codegen integration.

cover

Dec 22Tutorial

Writing Your First TurboModule in Swift

Learn how to build a fully working TurboModule in Swift, integrate it with Codegen, and bridge it through Objective‑C inside an Expo app.