Automating CodePush deploys with Fastlane
CodePush is a great service that allows you to update your React Native apps over the air without waiting on the user to update their app through the App Store or Google Play. CodePush is extremely configurable, and includes a CLI to configure and initialize deploys. Despite a simple release-react
command and some good defaults, there are lots of questions and parameters to remember, such as:
- What track should I use (dev, staging, or production)?
- What version of the app should this deploytarget (what could break by releasing this)?
- Do I want to force-apply this update immediately and not wait until the next time the app resumes?
Why Automate?
A common thread exists between every software developer, regardless of experience. We’re all human, and humans make mistakes.
The best way to reduce the likelihood of a mistake is to automate as much as possible in your workflows and processes. In the React Native world, the best deploy-related tool we’ve found to do that is fastlane.
NOTE: If your impatient, and just want to see the full script we’re using, skip to the bottom of the page. For more detail on each piece, read on.
Create Codepush Lanes
We’ll start by creating our fastlane entry points, a lane called codepush_ios
and a lane called codepush_android
.
The only job of those lanes is to pass an app name param to a private lane (think of these as private methods).
desc "Codepush release to iOS" lane :codepush_ios do |options| CODEPUSH_APP = "MyOrg/MyApp-ios" do_codepush(app_name: CODEPUSH_APP, manditory: options[:manditory]) end desc "Codepush release to Android" lane :codepush_android do |options| CODEPUSH_APP = "MyOrg/MyApp-android" do_codepush(app_name: CODEPUSH_APP, manditory: options[:manditory]) end
The Main Script
The main part of our script first checks to make sure there are no local git modifications, and wraps the built-in release-react
command with a nice confirmation message. This gives a user a chance to make sure everything is correct before continuing.
To keep things clean, we handle getting the correct CodePush environment and app version from other private lanes.
private_lane :do_codepush do |options| ensure_git_status_clean environment = select_codepush_environment(options[:app_name]) version = select_app_version(project_name: project_name) manditory = !!options[:manditory] manditory_string = manditory ? " -m" : "" if UI.confirm("About to CODEPUSH your local branch *#{git_branch}* to the *#{options[:environment]}* environment for users running version #{options[:version]}. Proceed?") Dir.chdir("..") do sh "appcenter codepush release-react -a #{options[:app_name]} -d #{options[:environment]} -t #{options[:version]}#{manditory_string}" do |status, result, command| unless status.success? UI.error "Command #{command} failed with status #{status.exitstatus}" end UI.success "🚀 All done! Check out the rollout & install stats in the Codepush section of the dashboard on App Center." end end else UI.error "😅 that was close!" end end
Select a Proper Codepush Environment
These private lanes handle fetching the available CodePush environments and prompting the user to select one. By default AppCenter will create a Staging and Production environment, but you can create as many as you’d like.
private_lane :select_codepush_environment do |options| available_environments = fetch_codepush_environments(app_name: options[:app_name]) environment_labels = available_environments.map{|e| e.first} UI.select("What environment do you want to target?", environment_labels) end # fetch codepush environments. assumes the user has logged in to appcenter cli # via `appcenter login` private_lane :fetch_codepush_environments do |options| FETCH_ENV_COMMAND = "appcenter codepush deployment list -a APPNAME --output json" UI.message "Fetching Codepush environments for #{options[:app_name]}" sh FETCH_ENV_COMMAND.sub(/APPNAME/, options[:app_name]) do |status, result, command| unless status.success? UI.error "Command #{command} failed with status #{status.exitstatus}. Are you logged in via `appcenter login`?" end JSON.parse(result) end end
Select a Target App Version
This private lane will parse the current app version (it currently assumes iOS and Android are versioned in lockstep), and prompt the user for a target version.
Assuming the current app is version 1.3.0, selecting “All users” will apply the codepush update to every version. Selecting “most recent major” and “most recent minor” will target 1.x.x
and 1.3.x
respectively, and “Current” will target 1.3.0
only. To make sure this is clear to the user, we show the version target in the select prompt.
private_lane :select_app_version do |options| # Assumes symver (x.x.x) or 💥 current_version = get_version_number(xcodeproj: "./ios/#{project_name}.xcodeproj", target: options[:project_name]) current_major = [current_version.split('.').first, 'x', 'x'].join('.') current_minor = current_version.split('.').slice(0, 2).push('x').join('.') target_version_label = UI.select("What version do you want to target?", [ "All users", "Most recent major (#{current_major})", "Most recent minor (#{current_minor})", "Current (#{current_version})", ]) next "\"*\"" if target_version_label.match?(/All/) next current_major if target_version_label.match?(/major/) next current_minor if target_version_label.match?(/minor/) current_version end
That’s all folks!
Putting this all together, you now just have to run fastlane codepush_ios
and fastlane codepush_android
, answer a few questions, and watch it 🚀!
Our script in action - Part 1
Our script in action — Part 2
Here’s the full script:
project_name = 'MyApp' # To release a new version of the app via CodePush, run `bundle exec fastlane codepush_ios` # or `bundle exec fastlane codepush_android` from the proper branch. For example: # If you want to CodePush to the production (release) version, make sure and run this from the release branch. # If you want to CodePush to the beta version, make sure to run this from the beta branch. # # Generally speaking, you should use the Staging track to validate a production Codepush before it is released / promoted. # # To mark an update as manditory, which will immediately download, apply the update, and restart the users app, # pass manditory:true. `bundle exec fastlane codepush_ios manditory:true` desc "Codepush release to iOS" lane :codepush_ios do |options| CODEPUSH_APP = "MyOrg/MyApp-ios" do_codepush(app_name: CODEPUSH_APP, manditory: options[:manditory]) end desc "Codepush release to Android" lane :codepush_android do |options| CODEPUSH_APP = "MyOrg/MyApp-android" do_codepush(app_name: CODEPUSH_APP, manditory: options[:manditory]) end private_lane :select_codepush_environment do |options| available_environments = fetch_codepush_environments(app_name: options[:app_name]) environment_labels = available_environments.map{|e| e.first} UI.select("What environment do you want to target?", environment_labels) end # fetch codepush environments. assumes the user has logged in to appcenter cli # via `appcenter login` private_lane :fetch_codepush_environments do |options| FETCH_ENV_COMMAND = "appcenter codepush deployment list -a APPNAME --output json" UI.message "Fetching Codepush environments for #{options[:app_name]}" sh FETCH_ENV_COMMAND.sub(/APPNAME/, options[:app_name]) do |status, result, command| unless status.success? UI.error "Command #{command} failed with status #{status.exitstatus}. Are you logged in via `appcenter login`?" end JSON.parse(result) end end private_lane :select_app_version do |options| # Assumes symver (x.x.x) or 💥 current_version = get_version_number(xcodeproj: "./ios/#{project_name}.xcodeproj", target: options[:project_name]) current_major = [current_version.split('.').first, 'x', 'x'].join('.') current_minor = current_version.split('.').slice(0, 2).push('x').join('.') target_version_label = UI.select("What version do you want to target?", [ "All users", "Most recent major (#{current_major})", "Most recent minor (#{current_minor})", "Current (#{current_version})", ]) next "\"*\"" if target_version_label.match?(/All/) next current_major if target_version_label.match?(/major/) next current_minor if target_version_label.match?(/minor/) current_version end private_lane :do_codepush do |options| ensure_git_status_clean environment = select_codepush_environment(options[:app_name]) version = select_app_version(project_name: project_name) manditory = !!options[:manditory] manditory_string = manditory ? " -m" : "" if UI.confirm("About to CODEPUSH your local branch *#{git_branch}* to the *#{options[:environment]}* environment for users running version #{options[:version]}. Proceed?") Dir.chdir("..") do sh "appcenter codepush release-react -a #{options[:app_name]} -d #{options[:environment]} -t #{options[:version]}#{manditory_string}" do |status, result, command| unless status.success? UI.error "Command #{command} failed with status #{status.exitstatus}" end UI.success "🚀 All done! Check out the rollout & install stats in the Codepush section of the dashboard on App Center." end end else UI.error "😅 that was close!" end end
To learn more about our mobile development process at Echobind, visit our React Native capabilities page.