disclaimer: This post will cover the main details about how to set up Continuous Deployment when you're developing mobile apps, specifically using ReactNative to build android and iOS applications, however consider that the same configuration (with some pertinent modifications) would work for native deployment.
Deploying mobile apps with the current state of the technology can be not only cumbersome but very stressful, you need to keep track of certificates, profiles, specific platform configurations, plus internal requirements like unit tests or integration tests and specific environments like QA, Release, Development or even an environment to deploy certain features for review and test (iOS call these environments schemas while android call them flavors)
If you have just one flavor but want to have the app deployed for testing (TestFlight in AppStore and alpha/beta testing in PlayStore) you could think all of this is unnecessary and a simple configuration is good enough or even manual deploy could be the proper solution but that's not true, and pay attention to this, in the long term having automatic deployments well configured, even if it's only one environment, will help you to have an stable environment, to focus on the architecture (this is something that drives my interest and where I like to focus most of the time) instead of trying to figure out why the app is not being built or deployed.
what's not this post about?
This post won't detail each of the steps, mainly because most of the things are specific to the tools and the needs you could have, and for that there's always documentation, forums and your experience.
This post is not an evangelic posture regarding Jenkins + Fastlane, it will present these tools because they're the ones being use in VitalBeats and are the ones that give us the confidence, stability and maintainability to keep deploying our products, however the core concepts should be translatable.
Let's work!
Let's enumerate the tools/environments involved during this process:
Github
Your source code should be stored in a version manager system, but not only that, your private keys, env_vars, certificates should stored in a private repo and must be encrypted (always be concerned with security), with that in mind you should keep in mind some env_vars should be set in the deployment environment (i.e. Jenkins) and read those from your deployment environment, for everything else, stored them encrypted.
Here in VitalBeats we use fastlane.match for iOS certificates and profiles (more about this below) and for everything else (android keys, iOS and android env_vars, GoogleServices plist files) fastlane.cryptex
you can follow the documentation to understand how to set up those repos based on your project requirements
I'd suggest following repo structure for iOS:
- certificates branch: as stated in fastlane.match you should have:
certs/
| development/
| <CertificateID>.cer
| <CertificateID>.p12
| distribution/
| <CertificateID>.cer
| <CertificateID>.p12
profiles/
| development/
| Development_<identifierId>.mobileprovision // i.e. Development_com.foo<.flavor>.mobileprovision
| appstore/
| AppStore_<identifierId>.mobileprovision // flavor(s) you'll deploy, i.e AppStore_com.foo.<qa|dev>.mobileprovision
- env_vars branch (using fastlane.cryptex)
GoogleService-Info-<flavor>.plist.crypt // most probably you'll be using firebase PN and any other service
env-<flavor>.crypt // env_vars like URL, internal keys
fastlane-env.crypt // env_vars used by fastlane
This is the suggested repo structure for android:
- <flavor> branch: because we're using fastlane.cryptex for this platform, all documents should be stored in each <flavor> branch
api-playstore.json.crypt
env.crypt
google-services.json.crypt
release.keystore.crypt
fastlane
After several attempts we arrived to an structured, modular and consistent way to have fastlane in our product that allow us to have the platform specifics separated without conflicting between them:
root/
| Gemfile // cocoapods, fastlane, etc
| Jenkinsfile // more about this file below
| fastlane/
| pluginfile // list of plugins use by both platforms
| ...
| android/
| fastlane
| Cryptexfile // created when cryptex is configured
| Fastfile // lanes defined and structure
| Appfile
| ios/
| fastlane
| Appfile
| Cryptexfile
| Deliverfile // to configure AppStore metadata
| Gymfile
| Fastfile // lanes defined and structured
| Matchfile
keep in mind
When deploying to the AppStore (iOS) to deploy the app to the store not just TestFlight you'll need to enable and act with 2FA in mind you should use fastlane.spaceauth to get a session cookie that will be used by fastlane to upload the build and set it for Store Deployment (the session cookie will be used when Jenkins is configured)
Sadly the process to get and set the session cookie must be done every time you want to publish a build to the store (to start the review process which must be done directly from the AppStore site) because it has an expiration time of 24 hours
Fastlane, specifically the fastfile
source code must have configured everything related to build and to deploy the app, it should specify how to get the certificates, what keys to use, where to save the build, when to upload to TestFlight and when to deploy as a potential release to store, it must have the credentials to upload the build to any of the stores.
Ideally it also should send slack notifications (or any kind of notification for what is worth) about the state of the build, if it failed or if it succeeded.
When configuring Fastlane do it without any CI server in mind, think: this is the way to build and I'll have to run the command manually, this way of thinking will help you to keep things isolated in the right places
Build Server
Before detailing Jenkins configuration we need to set up and prepare our Build Server (here at VitalBeats we use macstadium but you could use even your local computer).
Before setting up Jenkins to work with this computer as a node you'll need to prepare the build and deployment environment so here are the simplified steps and notes I hope will help you:
- Clone your app repo in a logical and consistent directory (I like to create a
Developer/
directory inside/Users/<Home>
) - Install dependencies and run
pod install
- Open XCode, sign in with the proper account (don't do anything else here)
- Run a build for iOS (in every flavor you have) from the console, if you configured correctly fastlane (with its plugins) the certificates will be installed and a prompt will appear asking you the password to store them in the
Keychain
app - Do the same but for android
- Create a directory (ideally inside the
~/Developer
directory) namedjenkins/
or something meaningful (this will be the directory used by Jenkins to manage the builds)
NOTE: always enable, with proper security, ssh access to your build server, most of the time is more convenient to access it through ssh instead of using vnc and consuming resources unnecessarily
NOTE 2: Leave the cloned project you used to configure and install dependencies and certificates, it will help you to debug potential issues that could happen and specially it will help you to get the session cookie
knowing it's the standalone project and not the one managed by Jenkins
// The command to get the session cookie must be
// executed inside the ./ios directory
$> fastlane spaceauth -u foo@bar.com
// You must configure your apple developer account to
// use a phone number to send the auth code in order
// to receive the session cookie
NOTE 3: there'll be some keys and env_vars you'll have to add and configure in your build server, i.e..zshrc
or.bash_profile
or.vimrc
to ease the access and management of the computer and those files will be needed every time you need to configure another computer or the same if you format it, so, and this is important, use dotfiles and store them in a private repo, that way next time you'll only need access to that repo, execute abootstrap
script and everything will be in place
Jenkins
yeah!!! we finally arrived to the special piece, the one that will automate our whole process and will ease our life :)
Configuring Jenkins is not difficult, it's a little complex because the UI (and this is my personal opinion) is not friendly but with attention, the tool can be properly configured to do whatever you want.
The first thing is to configure the node (the build server you configured previously). You'll need to follow the steps to configure Jenkins service, once that's done, you'll need to create a node, which will be the build server, and for that Jenkins will give you a secret key you need to store and execute next to the agent.jar
file.
As mentioned before, the UI is not too friendly so if you can't find the link to download the agent.jar file here's the link
I'd suggest to useAutomator app
to create an app that will locate theagent.jar
file and execute it with the secret key you just got, this app will be accessed more easily and you or any other person will only need to open it in order to start, or restart, the node
Once you have confirmation the node is running (Jenkins UI will inform you about that), open the node and go to its configurations - Configure
link - and add here those keys or env_vars that need to be passed to the Build Server.
One important env_var is FASTLANE_SESSION (or the name you want to use) that will be used to upload your build to the AppStore for release (explained above)
Once you have all this configured, open your fastfile
source code and configure it, considering following constraints:
- Jenkins should only be used to access via ssh to your build server and execute
fastlane
commands - Separate stages like
install
test
build iOS
andbuild android
- Jenkins should stop everything if, let's say,
install
andtest
stages fail - Jenkins should build
iOS
andandroid
independently, this means, ifiOS
fails it should buildandroid
With that in mind I'd suggest to use sshagent
plugin to access the Build Server and catchError
plugin for iOS/android
independent builds.
And that's it, if you read the documentation, planned properly and configured consciously you'll have an environment that will help you to improve your development and will ease the distribution of your app :D