A year ago I wrote a post about how Fastlane could help us to improve our React Native apps shipping process. At that moment even though everything was automated, the deployment relied on one of us with a provisioned machine in order to launch the rocket 🚀. We could improve easily that process by continuously delivering our apps through a CI machine. That's when Travis CI comes to the rescue! 👷🏻♂️
Before explaining what's the problem, it's important to understand the complexity of our deployment process.
In a nutshell we have two platforms: iOS 🍏, Android 🤖 and every platform compiles two applications: Beta testing app also known as Canary 🐤 and Production 🚀 one.
Basically every platform goes through a lane sequantially that looks like this 👇
- Code sign setup ✍️
- Version management 🔖
- Native builds 📦
- Beta testing distribution 🐤
- Stores distribution 🚀
- Sourcemaps 🗺
- Communication 🗣
Now let's see in depth every step of the deployment process to understand what we do.
Code sign setup ✍️
We adopted the codesigning.guide concept through Fastlane. Basically this idea comes up with having a specific git repository to store and distribute certificates across a development team. We store both iOS and Android code signing files on an encrypted private git repository that lives on GitHub.
Then, our CI machine on every deploy clones the repository and installs the decrypted certificates. On iOS the CI creates an OS X Keychain where the certificates are installed.
Version management 🔖
Native builds and stores require code version bumps.
Every platform has his own way to manage versions and build numbers. The difference between those two is that the version should be used as the public store number that identifies a new release, and the build number is an incremental identifier that bumps on every build.
- Public version number:
- Build numbers:
- Public version number:
- Build numbers:
Those attributes are stored on
.gradle files. To automate and do version management we use the package.json version number as the source of truth for our public version numbers 💯. This allows us to use
npm version cli command to manage bumps.
Native builds 📦
We need to provision two machines to build and compile our native applications.
For iOS we setup a macOS system with Xcode, because it's the only way to compile and sign the application. On Android we provision a Linux system, with all the Android Studio, packages and tools that we need.
Those machines are created by our CI, that means every build runs on a new fresh and clean environment 💻.
Beta testing distribution 🐤
Stores distribution 🚀
To get human readable information about crashes and errors, we use a service called Bugsnag. Every time we deploy a new build, we need to upload debug symbols
.dSYM and sourcemaps to Bugsnag.
Finally, when the apps are deployed, we need to inform our beta testers, release manager and developers, that we have a new version. We use Slack with a bot that sends alerts to some channels.
Every time we wanted to do a release, we had to manually fire 🔥 the Fastlane deployment lanes. That means that human factor was needed. This was a time consuming process that often failed due to code sign, biased environments, software updates, native platform dependencies...
Machines should work, people should think.
Definitely we decided to end with those problems by automating all the things!
The solution is to implement this automated process into a system that continously delivers our
master branch pushes up to the stores magically 🎉, giving freedom to the manager to decide when a new release comes up. Finally, we could forget about everything and be happy! ❤️
Now we're going to take a look on how we integrated Travis and Fastlane to automate the deployment of our apps 👏.
We have two
deployment lanes one for Android and one for iOS. I've simplified the lanes a little bit for the explanation to focus on the important parts of it. First we deploy Android platform and then iOS.
The lane receives a version number that comes from the
package.json, as I said before this allows us to do versioning through npm.
The first thing we do is bumping the public version number and the build number. On the iOS lane, we need to
setup_certificates, to save them on the Keychain and be able to sign the apps.
After that we start the
canary 🐤 and
production 🚀 lanes. Those two are the ones who build the native app.
Canary: Beta testing build, ships to TestFlight and HockeyApp.
Production: Production build, ships to TestFlight and Google Play Store.
Then, we upload all the sourcemaps and debug symbol files to Bugsnag.
Next, we create a git branch where the version bumps will be commited, through the
commit_and_push_version_bump lane. Later, on the iOS lane we merge the created git branch on top of
master using the
git_flow_merge lane. We need to commit the bumps, in order to maintain the version along with the deployments. Otherwise the stores should throw an error that the uploaded version already exists!
Finally we reach out Slack, to communicate both deployments.
lane :deployment do |version: version| bump_version_number(version: version) canary production sh 'npm run repositories:upload:android' commit_and_push_version_bump slack_notification(platform: 'Android', version: version) end
lane :deployment do |version: version| setup_certificates bump_version_number(version: version) canary production sh 'npm run repositories:upload:ios' commit_and_push_version_bump git_flow_merge(version: version) slack_notification(platform: 'iOS', version: version) end
So, here's how our git log, looks like after merging a branch to
master and making a deploy 🙌:
We use build stages, to run our deployment process in three steps, sequentially. This allows us to deploy our apps only on the
master branch when our tests passed ✅.
Let's take a look at the build stages 👇
Every build stage has his own provisioning and enviroment. For instance,
Deploy iOS runs on a macOS machine with Xcode and Node.js installed, while
Deploy Android uses an Ubuntu machine with JDK, AndroidSDK and Node.js.
Test stage ✅
On the first stage we execute the linters and test suites. To ensure everything is working as expected. If something fails here, we automatically stop the deploy.
- stage: Test and lint ✅ language: node_js node_js: 8.5.0 install: yarn script: npm run test:lint && npm run test:unit
Android stage 🤖
Android stage creates a provisioned Ubuntu machine with all the software and dependencies needed. Then we build the Canary 🐤 and Production 🚀 applications apps. After that we deploy them. In around 15 minutes ⏰ our Android apps ship. 👏
- stage: Deploy Android 🤖 if: branch = master AND type = push language: android jdk: oraclejdk8 android: components: - tools - platform-tools - android-26 - extra-google-m2repository - extra-google-google_play_services before_install: - nvm install 8.5.0 - gem install bundler - bundle install before_script: - ./internals/scripts/travis/gitconfig.sh install: yarn script: npm run deployment:android
iOS stage 🍏
iOS stage creates a provisioned macOS machine with Xcode and all the dependencies needed. Then we build the Canary 🐤 and Production 🚀 apps. After that we deploy them. In around 20 minutes ⏰ our iOS apps ship. 👏
- stage: Deploy iOS 🍏 if: branch = master AND type = push language: node_js node_js: 8.5.0 os: osx osx_image: xcode9.2 before_install: bundle install before_script: - ./internals/scripts/travis/gitconfig.sh install: yarn script: npm run deployment:ios
- Avoid human factor as much as you can, by automating all the things!
- Native ecosystem is tough, sometimes kind of frustrating and you should accept that. It's not our expertise since we're JS devs, but there's a lot of people and documentation that helps out.
- Make processes.