Skip to content

Instantly share code, notes, and snippets.

@harrisrobin
Created August 9, 2023 09:56
Show Gist options
  • Save harrisrobin/8a332978dcbcbc6ac831d63147a10055 to your computer and use it in GitHub Desktop.
Save harrisrobin/8a332978dcbcbc6ac831d63147a10055 to your computer and use it in GitHub Desktop.
Fastlane CI/CD automation suite for React-Native. Supports multiple envs (staging app & prod app), slack notifications, Sentry sourcemaps & dsym uploading, codepush etc. Very extensive and handles all release/beta requirements.
require "json"
require "httparty"
require "sem_version"
require 'xcodeproj'
#
# CONSTS
#
FASTLANE_ENV_FILE = ".fastlane"
PROJECT = "REDACTED"
PROJECT_STAGING = "REDACTED"
VERSION_NUMBER_NEXT = "1.0.0" # do not change
VERSION_NUMBER_ANDROID = "1.0.0" # do not change
VERSION_NUMBER_IOS = "1.0.0" # do not change
BUILD_NUMBER_NEXT = 1 # do not change
BUILD_NUMBER_ANDROID = 1 # do not change
BUILD_NUMBER_IOS = 1 # do not change
XCODE_SCHEME = PROJECT
XCODE_SCHEME_STAGING = PROJECT_STAGING
XCODE_PROJECT = "ios/#{PROJECT}.xcodeproj"
XCODE_WORKSPACE = "ios/#{PROJECT}.xcworkspace"
IOS_BADGE_DIR = "ios/#{PROJECT}/Images.xcassets"
ANDROID_BADGE_DIR = "android/app/src/main/res"
ANDROID_KEYSTORE_FILE = "android/app/redacted-upload.keystore"
ANDROID_KEYSTORE_PROPERTIES_FILE = "android/app/keystore.properties"
GOOGLE_PLAY_JSON_FILE = "android/play-store-credentials.json"
AAB_DIR_STAGING = "android/app/build/outputs/bundle/stagingRelease"
AAB_DIR_PRODUCTION = "android/app/build/outputs/bundle/productionRelease"
BUILD_DIR = "build"
ARTIFACTS_DIR = "artifacts"
SLACK_CHANNEL = "REDACTED"
AWS_EXPORTS_FILE = "../aws-exports.js"
AWS_PROD_COGNITO_USER_POOL_ID = "REDACTED"
AWS_DEV_COGNITO_USER_POOL_ID = "REDACTED"
INFO_PLIST_PATH = "ios/#{PROJECT}/Info.plist"
# Note(Harris): the '' are necessary to escape the space in the filename.
STAGING_INFO_PLIST_PATH = '"ios/redacted staging-Info.plist"'
GRADLE_FILE_PATH = "android/app/build.gradle"
PROD_BUNDLE_ID = "com.redacted"
STAGING_BUNDLE_ID = "com.redacted"
VERSION_TS_FILE = "../app/utils/constants/version.ts"
BOT_TOKEN = ENV["BOT_TOKEN"]
APP_CENTER_LOGIN_TOKEN = ENV["APP_CENTER_LOGIN_TOKEN"]
CODEPUSH_IOS_STAGING_DEPLOYMENT_KEY = ENV["CODEPUSH_IOS_STAGING_DEPLOYMENT_KEY"]
CODEPUSH_IOS_PRODUCTION_DEPLOYMENT_KEY = ENV["CODEPUSH_IOS_PRODUCTION_DEPLOYMENT_KEY"]
CODEPUSH_IOS_APP_NAME = "REDACTED"
CODEPUSH_ANDROID_APP_NAME = "REDACTED"
if File.exist?("#{FASTLANE_ENV_FILE}")
open("#{FASTLANE_ENV_FILE}").readlines.each { |l| kv = l.split("=", 2); ENV[kv[0]] = kv[1].strip }
end
lane :codepush_deploy_ios do |options|
track = options[:track]
platform = options[:platform]
get_version_and_build_numbers
DEPLOYMENT_NAME = track == "alpha" ? "Staging" : "Production"
PLIST_PATH = track === "alpha" ? STAGING_INFO_PLIST_PATH : INFO_PLIST_PATH
APP_NAME = CODEPUSH_IOS_APP_NAME
BUNDLE_ID_TO_USE = track == "alpha" ? STAGING_BUNDLE_ID : PROD_BUNDLE_ID
UI.message("Deploying to CodePush #{DEPLOYMENT_NAME}...")
UI.message("Track: #{track}, Platform: #{platform}")
codepush_login
## Release RN bundle
codepush_release_react(
app_name: "#{APP_NAME}",
deployment_name: "#{DEPLOYMENT_NAME}",
plist_file: "#{PLIST_PATH}",
sourcemap_output: "./build/main.jsbundle.map",
output_dir: "./build",
mandatory: false,
target_binary_version: "#{VERSION_NUMBER_NEXT}",
)
# Export sentry.properties
# both are necessary, ENV is required for fastlane to have it in it's
# process.
sh("export SENTRY_PROPERTIES=../ios/sentry.properties")
ENV["SENTRY_PROPERTIES"] = "../ios/sentry.properties"
# Upload outputted bundle to sentry
sh("sentry-cli react-native appcenter #{APP_NAME} ios ../build/CodePush --deployment #{DEPLOYMENT_NAME} --dist #{BUILD_NUMBER_NEXT} --bundle-id #{BUNDLE_ID_TO_USE} --version-name #{VERSION_NUMBER_NEXT} --log-level debug")
end
lane :codepush_deploy_android do |options|
track = options[:track]
platform = options[:platform]
get_version_and_build_numbers
DEPLOYMENT_NAME = track == "alpha" ? "Staging" : "Production"
APP_NAME = CODEPUSH_ANDROID_APP_NAME
BUNDLE_ID_TO_USE = track == "alpha" ? STAGING_BUNDLE_ID : PROD_BUNDLE_ID
UI.message("Deploying to CodePush #{DEPLOYMENT_NAME}...")
UI.message("Track: #{track}, Platform: #{platform}")
codepush_login
## Release RN bundle
codepush_release_react(
app_name: "#{APP_NAME}",
deployment_name: "#{DEPLOYMENT_NAME}",
gradle_file: "#{GRADLE_FILE_PATH}",
sourcemap_output: "./build/index.android.bundle.map",
output_dir: "./build",
mandatory: false,
target_binary_version: "#{VERSION_NUMBER_NEXT}",
)
# Export sentry.properties
# both are necessary, ENV is required for fastlane to have it in it's
# process.
sh("export SENTRY_PROPERTIES=../android/sentry.properties")
ENV["SENTRY_PROPERTIES"] = "../android/sentry.properties"
# Upload outputted bundle to sentry
sh("sentry-cli react-native appcenter #{APP_NAME} ios ../build/CodePush --deployment #{DEPLOYMENT_NAME} --dist #{BUILD_NUMBER_NEXT} --bundle-id #{BUNDLE_ID_TO_USE} --version-name #{VERSION_NUMBER_NEXT} --log-level debug")
end
lane :codepush_promote_to_production do |options|
platform = options[:platform]
APP_NAME = platform === "ios" ? CODEPUSH_IOS_APP_NAME : CODEPUSH_ANDROID_APP_NAME
## Promote deployment
codepush_promote(
app_name: "#{APP_NAME}",
source_deployment_name: "Staging",
destination_deployment_name: "Production"
)
end
lane :test_fastlane do |options|
# Get keystore password from keystore.properties file
ANDROID_KEYSTORE_PASS = sh("grep -E 'storePassword' ../#{ANDROID_KEYSTORE_PROPERTIES_FILE} | cut -d'=' -f2").strip
ANDROID_KEYSTORE_ALIAS = sh("grep -E 'keyAlias' ../#{ANDROID_KEYSTORE_PROPERTIES_FILE} | cut -d'=' -f2").strip
track = :alpha
AAB_DIR = track == :alpha ? AAB_DIR_STAGING : AAB_DIR_PRODUCTION
AAB_FILE_NAME = track == :alpha ? "app-staging-release.aab" : "app-production-release.aab"
APK_FILE_NAME = track == :alpha ? "app-staging-release.apk" : "app-production-release.apk"
bundletool(
ks_path: ANDROID_KEYSTORE_FILE,
ks_password: ANDROID_KEYSTORE_PASS,
ks_key_alias: ANDROID_KEYSTORE_ALIAS,
ks_key_alias_password: ANDROID_KEYSTORE_PASS,
bundletool_version: "1.15.2",
aab_path: "#{AAB_DIR}/#{AAB_FILE_NAME}",
apk_output_path: "#{AAB_DIR}/#{APK_FILE_NAME}",
verbose: true
)
UI.message("generating APK...")
end
lane :notify_slack do |options|
platform = options[:platform]
track = options[:track]
AAB_FILE_NAME = track == :alpha ? "app-staging-release.aab" : "app-production-release.aab"
APK_FILE_NAME = track == :alpha ? "app-staging-release.apk" : "app-production-release.apk"
slack_url = ENV["SLACK_APP_NOTIFICATIONS_WEBHOOK_URL"]
UI.message("Sending message to Slack channel...")
PROD_BUNDLE_EXPLORER = "REDACTED"
STAGING_BUNDLE_EXPLORER = "REDACTED"
PROD_RELEASES_DASHBOARD = "REDACTED"
STAGING_RELEASES_DASHBOARD = "REDACTED"
PROD_TESTFLIGHT_URL = "REDACTED"
STAGING_TESTFLIGHT_URL = "REDACTED"
headers = {
'Content-Type' => 'application/json; charset=utf-8',
'Authorization' => "Bearer #{BOT_TOKEN}"
}
body = {
channel: "#{SLACK_CHANNEL}",
text: "*Notify:* <@U04EZ5C2VSA> <@U04F2Q58RQD>",
blocks: [{
"type": "header",
"text": {
"type": "plain_text",
"text": "v#{VERSION_NUMBER_NEXT}+build#{BUILD_NUMBER_NEXT}",
},
}, {
"type": "section",
"fields": [
{
"type": "mrkdwn",
"text": "*Platform:* #{platform.to_s}",
},
{
"type": "mrkdwn",
"text": "*Track:* #{track.to_s}",
},
{
"type": "mrkdwn",
"text": "*Server Env:* #{track == :alpha ? "staging" : "production"}",
},
{
"type": "mrkdwn",
text: "*Notify:* <@U04EZ5C2VSA> <@U04F2Q58RQD> <@U04EZ5BVAR4> <@U04FJCAAS9X>",
},
],
"accessory": {
"type": "image",
"image_url": platform == :ios ? "https://www.freepnglogos.com/uploads/apple-logo-png/apple-logo-icon-transparent-png-svg-vector-3.png" : "https://www.freepnglogos.com/uploads/android-logo-png/android-logo-powerful-mobile-apps-for-those-with-disabilities-3.png",
"alt_text": "logo",
},
}, {
"type": "actions",
"elements": platform == :ios ? [{
"type": "button",
"text": {
"type": "plain_text",
"text": "Open TestFlight",
},
"style": "primary",
"url": "#{track == :alpha ? "#{STAGING_TESTFLIGHT_URL}" : "#{PROD_TESTFLIGHT_URL}" }",
}] : [
{
"type": "button",
"text": {
"type": "plain_text",
"text": "Open Bundle Explorer",
},
"url": "#{track == :alpha ? STAGING_BUNDLE_EXPLORER : PROD_BUNDLE_EXPLORER }",
},
{
"type": "button",
"text": {
"type": "plain_text",
"text": "Open Release Dashboard",
},
"style": "primary",
"url": "#{track == :alpha ? STAGING_RELEASES_DASHBOARD : PROD_RELEASES_DASHBOARD }",
},
],
}].to_json
}.to_json
response = HTTParty.post("https://slack.com/api/chat.postMessage", body: body, headers: headers)
if response["ok"]
UI.success("Message sent to Slack channel successfully!")
else
UI.error("Failed to send message to Slack channel: #{response['error']}")
end
if platform == :android
HTTParty.post("https://slack.com/api/files.upload",
headers: {
"Content-Type" => "multipart/form-data",
},
body: {
token: BOT_TOKEN,
channels: "#{SLACK_CHANNEL}",
file: File.open("../#{ARTIFACTS_DIR}/#{VERSION_NUMBER_NEXT}/#{BUILD_NUMBER_NEXT}/android/#{track.to_s}/#{APK_FILE_NAME}"),
initial_comment: "*Android APK:*",
})
end
end
lane :generate_changelog_and_notify_slack do |options|
get_version_and_build_numbers
sh "cd .. && auto-changelog --unreleased-only --hide-credit --template compact"
sh "cd .. && md-to-pdf ./CHANGELOG.md &"
HTTParty.post("https://slack.com/api/files.upload",
headers: {
"Content-Type" => "multipart/form-data",
},
body: {
token: BOT_TOKEN,
file: File.open("../CHANGELOG.pdf"),
title: "Changelog for v#{VERSION_NUMBER_NEXT}",
channels: "#{SLACK_CHANNEL}",
initial_comment: "*Builds Completed!* :tada::tada::tada:",
})
end
lane :get_version_and_build_numbers do
verify_fastlane_env
verify_github_env
response = HTTParty.get("REDACTED",
headers: {
"Accept" => "application/vnd.github.v3+json",
"Authorization" => "token #{ENV["GITHUB_TOKEN"]}",
})
unless response.code == 200
UI.user_error!("Invalid Github token provided.")
exit(1)
end
begin
description = JSON.parse(response.body)["description"].strip
description.split(" ").each do |version|
version_string = version.gsub(/android@|ios@|next@|build/, "")
v = SemVersion.new(version_string)
if version.include?("android")
BUILD_NUMBER_ANDROID = v.metadata.to_i
v.metadata = nil
VERSION_NUMBER_ANDROID = v.to_s
elsif version.include?("ios")
BUILD_NUMBER_IOS = v.metadata.to_i
v.metadata = nil
VERSION_NUMBER_IOS = v.to_s
elsif version.include?("next")
BUILD_NUMBER_NEXT = v.metadata.to_i
v.metadata = nil
VERSION_NUMBER_NEXT = v.to_s
end
end
# Define the content of the TypeScript file
content = <<~HEREDOC
export const BUILD_NUMBER_ANDROID = "#{BUILD_NUMBER_ANDROID}"
export const VERSION_NUMBER_ANDROID = "#{VERSION_NUMBER_ANDROID}"
export const BUILD_NUMBER_IOS = "#{BUILD_NUMBER_IOS}"
export const VERSION_NUMBER_IOS = "#{VERSION_NUMBER_IOS}"
export const BUILD_NUMBER_NEXT = "#{BUILD_NUMBER_NEXT}"
export const VERSION_NUMBER_NEXT = "#{VERSION_NUMBER_NEXT}"
export const PROD_BUNDLE_ID = "#{PROD_BUNDLE_ID}"
export const STAGING_BUNDLE_ID = "#{STAGING_BUNDLE_ID}"
HEREDOC
# Write the content to the file
File.open(VERSION_TS_FILE, "w") { |file| file.write(content) }
rescue
UI.user_error!("Could not parse Github response for the version/build numbers.")
exit(1)
end
end
lane :verify_android_env do
unless File.exist?("../#{ANDROID_KEYSTORE_FILE}")
UI.user_error!("No keystore file provided.")
exit(1)
end
unless File.exist?("../#{ANDROID_KEYSTORE_PROPERTIES_FILE}")
UI.user_error!("No keystore.properties file provided.")
exit(1)
end
unless File.exist?("../#{GOOGLE_PLAY_JSON_FILE}")
UI.user_error!("No google-play.json file provided.")
exit(1)
end
end
lane :clean_js do
sh "rm -rf ../node_modules"
sh "yarn install --network-timeout 1000000"
end
lane :verify_amplify_env do |options|
unless File.exist?("#{AWS_EXPORTS_FILE}")
UI.user_error!("The file `aws-exports.js` was not found.")
exit(1)
end
track = options[:track]
contents = File.read(AWS_EXPORTS_FILE)
json_start_index = contents.index('{')
json_end_index = contents.rindex('}')
json_string = contents[json_start_index..json_end_index]
json_object = JSON.parse(json_string)
aws_user_pools_id = json_object["aws_user_pools_id"]
if track == :alpha
if aws_user_pools_id == AWS_DEV_COGNITO_USER_POOL_ID
UI.success("The 'aws_user_pools_id' value is equal to '\n#{AWS_DEV_COGNITO_USER_POOL_ID}'. You are using the right aws-exports.")
else
UI.error("The 'aws_user_pools_id' value is not equal to '\n#{AWS_DEV_COGNITO_USER_POOL_ID}'. Make sure you run amplify env checkout dev before running this lane.")
exit(1)
end
end
if track == :beta
if aws_user_pools_id == AWS_PROD_COGNITO_USER_POOL_ID
UI.success("The 'aws_user_pools_id' value is equal to '\n#{AWS_PROD_COGNITO_USER_POOL_ID}'. You are using the right aws-exports.")
else
UI.error("The 'aws_user_pools_id' value is not equal to '\n#{AWS_PROD_COGNITO_USER_POOL_ID}'. Make sure you run amplify env checkout prod before running this lane.")
exit(1)
end
end
end
lane :clean_ios do
sh "rm -rf ../ios/build"
end
lane :setup_pods do
sh "cd ../ios && bundle exec pod repo update && bundle exec pod install"
end
lane :setup_build_dir do
sh "rm -rf ../#{BUILD_DIR}" if Dir.exist?("../#{BUILD_DIR}")
Dir.mkdir "../#{BUILD_DIR}"
Dir.mkdir "../#{BUILD_DIR}/ios"
Dir.mkdir "../#{BUILD_DIR}/android"
end
lane :setup_artifacts_dir do
Dir.mkdir "../#{ARTIFACTS_DIR}" unless Dir.exist?("../#{ARTIFACTS_DIR}")
end
lane :verify_github_env do
unless ENV["GITHUB_TOKEN"]
UI.user_error!("No Github token provided.")
exit(1)
end
end
lane :verify_fastlane_env do
unless File.exist?("#{FASTLANE_ENV_FILE}")
UI.user_error!("The file `.fastlane` was not found.")
exit(1)
end
sentry_check_cli_installed()
end
lane :generate_badge do |options|
platform = options[:platform]
if platform == :ios
glob = "/#{IOS_BADGE_DIR}/AppIcon.appiconset/*.png"
elsif platform == :android
glob = "/#{ANDROID_BADGE_DIR}/mipmap-*/icon.png"
end
# For customization, see: https://github.com/HazAT/fastlane-plugin-badge
add_badge(shield: "#{VERSION_NUMBER_NEXT}-#{BUILD_NUMBER_NEXT}-red", alpha: true, glob: glob)
end
lane :create_sourcemap do |options|
platform = options[:platform]
sh "yarn react-native bundle \
--dev false \
--platform #{platform.to_s} \
--entry-file index.js \
--bundle-output #{BUILD_DIR}/#{platform.to_s}/#{platform == :ios ? "main.jsbundle" : "index.android.bundle"} \
--sourcemap-output #{BUILD_DIR}/#{platform.to_s}/#{platform == :ios ? "main.jsbundle.map" : "index.android.bundle.map"}"
end
lane :copy_build_to_artifacts do |options|
platform = options[:platform]
track = options[:track]
if platform == :ios
# For customization, see: https://docs.fastlane.tools/actions/copy_artifacts/
copy_artifacts(
target_path: "#{ARTIFACTS_DIR}/#{VERSION_NUMBER_NEXT}/#{BUILD_NUMBER_NEXT}/ios/#{track.to_s}",
artifacts: [
"#{BUILD_DIR}/ios/*.map",
"#{BUILD_DIR}/ios/*.ipa",
"#{BUILD_DIR}/ios/*.zip",
"#{BUILD_DIR}/ios/*.jsbundle",
],
fail_on_missing: true
)
elsif platform == :android
# For customization, see: https://docs.fastlane.tools/actions/copy_artifacts/
AAB_DIR = track == :alpha ? AAB_DIR_STAGING : AAB_DIR_PRODUCTION
AAB_FILE_NAME = track == :alpha ? "app-staging-release.aab" : "app-production-release.aab"
APK_FILE_NAME = track == :alpha ? "app-staging-release.apk" : "app-production-release.apk"
copy_artifacts(
target_path: "#{ARTIFACTS_DIR}/#{VERSION_NUMBER_NEXT}/#{BUILD_NUMBER_NEXT}/android/#{track.to_s}",
artifacts: [
"#{AAB_DIR}/#{AAB_FILE_NAME}",
"#{AAB_DIR}/#{APK_FILE_NAME}",
"#{BUILD_DIR}/android/*.map",
"#{BUILD_DIR}/android/*.bundle",
],
fail_on_missing: true
)
end
end
lane :upload_symbols do |options|
platform = options[:platform]
track = options[:track]
unless ENV["SENTRY_AUTH_TOKEN"]
UI.user_error!("The ENV var SENTRY_AUTH_TOKEN is not set.")
next
end
project_slug = "mobile-app"
# if track == :alpha then use staging project
PROJECT_TO_USE = track == :alpha ? PROJECT_STAGING : PROJECT
begin
# For customization, see: https://github.com/getsentry/sentry-fastlane-plugin
sentry_upload_dif(
auth_token: ENV["SENTRY_AUTH_TOKEN"],
org_slug: "REDACTED",
project_slug: project_slug,
include_sources: true,
wait: true, # Wait for the server to fully process uploaded files. Errors can only be displayed if --wait is specified, but this will significantly slow down the upload process.
)
sentry_create_release(
auth_token: ENV["SENTRY_AUTH_TOKEN"],
org_slug: "REDACTED",
project_slug: project_slug,
version: VERSION_NUMBER_NEXT,
build: BUILD_NUMBER_NEXT.to_s,
app_identifier: track === :alpha ? STAGING_BUNDLE_ID : PROD_BUNDLE_ID, # pass in the bundle_identifer of your app
finalize: true # Whether to finalize the release. If not provided or false, the release can be finalized using the sentry_finalize_release action
)
JS_BUNDLE_MAP = "#{ARTIFACTS_DIR}/#{VERSION_NUMBER_NEXT}/#{BUILD_NUMBER_NEXT}/#{platform.to_s}/#{track.to_s}/#{platform === :ios ? "main.jsbundle.map" : "index.android.bundle.map"}"
JS_BUNDLE = "#{ARTIFACTS_DIR}/#{VERSION_NUMBER_NEXT}/#{BUILD_NUMBER_NEXT}/#{platform.to_s}/#{track.to_s}/#{platform === :ios ? "main.jsbundle" : "index.android.bundle"}"
sentry_upload_sourcemap(
auth_token: ENV["SENTRY_AUTH_TOKEN"],
org_slug: "REDACTED",
project_slug: project_slug,
version: VERSION_NUMBER_NEXT,
dist: BUILD_NUMBER_NEXT.to_s,
sourcemap: [JS_BUNDLE, JS_BUNDLE_MAP],
rewrite: true, # Rewrite matching sourcemaps if they exist
)
sentry_set_commits(
version: VERSION_NUMBER_NEXT,
org_slug: "REDACTED",
project_slug: project_slug,
app_identifier: track === :alpha ? STAGING_BUNDLE_ID : PROD_BUNDLE_ID, # pass in the bundle_identifer of your app
build: BUILD_NUMBER_NEXT.to_s,
auto: true, # enable completely automated commit management
clear: false, # clear all current commits from the release
ignore_missing: true # Optional boolean value: When the flag is set and the previous release commit was not found in the repository, will create a release with the default commits count (or the one specified with `--initial-depth`) instead of failing the command.
)
FLAVOR = track == :alpha ? "stagingRelease" : "productionRelease"
# Uncomment if pro-guard is enabled
# if platform == :android
# sentry_upload_proguard(
# auth_token: ENV["SENTRY_AUTH_TOKEN"],
# org_slug: "REDACTED",
# project_slug: project_slug,
# android_manifest_path: "../android/app/build/intermediates/merged_manifests/#{FLAVOR}/AndroidManifest.xml",
# mapping_path: "../android/app/build/outputs/mapping/#{FLAVOR}/mapping.txt",
# )
# end
sentry_create_deploy(
auth_token: ENV["SENTRY_AUTH_TOKEN"],
org_slug: "REDACTED",
project_slug: project_slug,
version: VERSION_NUMBER_NEXT,
app_identifier: track === :alpha ? STAGING_BUNDLE_ID : PROD_BUNDLE_ID, # pass in the bundle_identifer of your app
build: BUILD_NUMBER_NEXT.to_s,
env: track == :alpha ? "staging" : "production" , # The environment for this deploy. Required.
)
rescue => ex
UI.user_error!("Something went wrong uploading debug symbols: #{ex}")
end
end
lane :update_sentry_release_and_dist do |options|
should_reset = options[:reset]
live_version = options[:live_version]
if live_version == :android
version = VERSION_NUMBER_ANDROID
build = BUILD_NUMBER_ANDROID
elsif live_version == :ios
version = VERSION_NUMBER_IOS
build = BUILD_NUMBER_IOS
else
version = VERSION_NUMBER_NEXT
build = BUILD_NUMBER_NEXT
end
if should_reset
version = ""
build = ""
end
end
lane :bump_version_or_build_number do |options|
segment = options[:segment]
get_version_and_build_numbers
v = SemVersion.new(VERSION_NUMBER_NEXT)
v = SemVersion.new(v.major, v.minor, v.patch, nil, BUILD_NUMBER_NEXT.to_s)
if segment == "major"
v.major = v.major + 1
v.minor = 0
v.patch = 0
end
if segment == "minor"
v.minor = v.minor + 1
v.patch = 0
end
if segment == "patch"
v.patch = v.patch + 1
end
if segment == "build"
v.metadata = (v.metadata.to_i + 1).to_s
end
next_v = SemVersion.new(v.major, v.minor, v.patch, nil, "build#{v.metadata}")
new_version_and_build_numbers = [
"next@#{next_v.to_s}",
"ios@#{VERSION_NUMBER_IOS}+build#{BUILD_NUMBER_IOS}",
"android@#{VERSION_NUMBER_ANDROID}+build#{BUILD_NUMBER_ANDROID}",
].join(" ")
response = HTTParty.post("REDACTED",
headers: {
"Accept" => "application/vnd.github.v3+json",
"Authorization" => "token #{ENV["GITHUB_TOKEN"]}",
},
body: {
description: new_version_and_build_numbers,
}.to_json)
unless response.code == 200
UI.user_error!("Could not bump build number.")
exit(1)
end
end
lane :bump_live_app_version_number do
git_branch_name = sh("git rev-parse --abbrev-ref HEAD").strip
if git_branch_name != "main"
UI.user_error!("This action should only be run on the `main` branch.")
exit(1)
end
get_version_and_build_numbers
should_bump_android = UI.confirm("Should \"#{VERSION_NUMBER_NEXT}\" be set as latest app version for Android?")
should_bump_ios = UI.confirm("Should \"#{VERSION_NUMBER_NEXT}\" be set as latest app version for iOS?")
next_version = UI.input("Please specify the next \"development\" version. This should be at least one \"patch\" level higher than both the Android and iOS live versions.")
android_version = should_bump_android ? VERSION_NUMBER_NEXT : VERSION_NUMBER_ANDROID
android_build = should_bump_android ? BUILD_NUMBER_NEXT : BUILD_NUMBER_ANDROID
ios_version = should_bump_ios ? VERSION_NUMBER_NEXT : VERSION_NUMBER_IOS
ios_build = should_bump_ios ? BUILD_NUMBER_NEXT : BUILD_NUMBER_IOS
next_build = BUILD_NUMBER_NEXT
if (SemVersion.new(next_version) <= SemVersion.new(ios_version) || SemVersion.new(next_version) <= SemVersion.new(android_version))
UI.user_error!("The next \"development\" version should be at least one \"patch\" level higher.")
exit(1)
end
new_version_and_build_numbers = [
"next@#{next_version}+build#{next_build}",
"ios@#{ios_version}+build#{ios_build}",
"android@#{android_version}+build#{android_build}",
]
UI.header("Confirm the new live app versions:")
UI.success("\n#{new_version_and_build_numbers.join("\n")}")
should_continue = UI.confirm("Should we proceed?")
if !should_continue
UI.error("Updating the live app versions was cancelled.")
else
response = HTTParty.post("REDACTED",
headers: {
"Accept" => "application/vnd.github.v3+json",
"Authorization" => "token #{ENV["GITHUB_TOKEN"]}",
},
body: {
description: new_version_and_build_numbers.join(" "),
}.to_json)
unless response.code == 200
UI.user_error!("Could not bump live version number on GitHub.")
exit(1)
end
if SemVersion.new(ios_version) == SemVersion.new(android_version) || SemVersion.new(ios_version) > SemVersion.new(android_version)
git_tag_version = ios_version
git_tag_build = ios_build
else
git_tag_version = android_version
git_tag_build = android_build
end
sh "git fetch --prune origin '+refs/tags/*:refs/tags/*'"
sh "git tag #{git_tag_version}+build#{git_tag_build} master"
sh "git push origin #{git_tag_version}+build#{git_tag_build}"
end
end
lane :export_sentry_env_variables do |options|
track = options[:track]
RELEASE_BUNDLE_ID = track == :alpha ? STAGING_BUNDLE_ID : PROD_BUNDLE_ID
RELEASE_NAME = "#{RELEASE_BUNDLE_ID}@#{VERSION_NUMBER_NEXT}+#{BUILD_NUMBER_NEXT}"
# Generate the content for the shell script
script_content = <<~HEREDOC
#!/bin/bash
export SENTRY_RELEASE="#{RELEASE_NAME}"
export SENTRY_DIST="#{BUILD_NUMBER_NEXT}"
HEREDOC
File.write("../scripts/sentry-release.sh", script_content)
sh "chmod +x ../scripts/sentry-release.sh"
sh "../scripts/sentry-release.sh"
ENV["SENTRY_RELEASE"] = "#{RELEASE_NAME}"
ENV["SENTRY_DIST"] = "#{BUILD_NUMBER_NEXT}"
end
# #
# # iOS
# #
platform :ios do
before_all do |lane, options|
verify_amplify_env(track: lane)
clean_js
clean_ios
setup_pods
get_version_and_build_numbers
export_sentry_env_variables(track: lane)
setup_build_dir
setup_artifacts_dir
update_sentry_release_and_dist
increment_build_number(build_number: BUILD_NUMBER_NEXT, xcodeproj: XCODE_PROJECT)
increment_version_number(version_number: VERSION_NUMBER_NEXT, xcodeproj: XCODE_PROJECT)
# For customization, see: https://docs.fastlane.tools/actions/match/
match(type: "appstore", api_key_path: "fastlane/app-store-connect.json")
end
after_all do |lane, options|
increment_build_number(build_number: 1, xcodeproj: XCODE_PROJECT)
increment_version_number(version_number: "1.0.0", xcodeproj: XCODE_PROJECT)
update_sentry_release_and_dist(reset: true)
sh "rm -rf .env"
notify_slack(platform: :ios, track: lane)
end
lane :compile do |options|
track = options[:track]
scheme_to_use = XCODE_SCHEME
begin
if track == :alpha
sh "git checkout ../#{IOS_BADGE_DIR}/"
generate_badge(platform: :ios)
scheme_to_use = XCODE_SCHEME_STAGING
end
create_sourcemap(platform: :ios)
# For customization, see: https://docs.fastlane.tools/actions/build_app/
gym(
configuration: "Release",
workspace: XCODE_WORKSPACE,
scheme: scheme_to_use,
silent: true,
clean: true,
output_directory: "#{BUILD_DIR}/ios",
)
ensure
if track == :alpha
sh "git checkout ../#{IOS_BADGE_DIR}/"
end
end
end
lane :alpha do |options|
track = :alpha
compile(track: track)
copy_build_to_artifacts(platform: :ios, track: track)
pilot(skip_waiting_for_build_processing: true, ipa: "#{ARTIFACTS_DIR}/#{VERSION_NUMBER_NEXT}/#{BUILD_NUMBER_NEXT}/ios/#{track.to_s}/#{PROJECT_STAGING}.ipa", api_key_path: "fastlane/app-store-connect.json") # For customization, see: https://docs.fastlane.tools/actions/upload_to_testflight/
upload_symbols(platform: :ios, track: track)
end
lane :beta do |options|
track = :beta
compile(track: track)
copy_build_to_artifacts(platform: :ios, track: track)
pilot(skip_waiting_for_build_processing: true, ipa: "#{ARTIFACTS_DIR}/#{VERSION_NUMBER_NEXT}/#{BUILD_NUMBER_NEXT}/ios/#{track.to_s}/#{PROJECT}.ipa", api_key_path: "fastlane/app-store-connect.json") # For customization, see: https://docs.fastlane.tools/actions/upload_to_testflight/
upload_symbols(platform: :ios, track: track)
end
end
#
# Android
#
platform :android do
before_all do |lane, options|
verify_amplify_env(track: lane)
get_version_and_build_numbers
export_sentry_env_variables(track: lane)
verify_android_env
setup_build_dir
setup_artifacts_dir
clean_js
# For customization, see: https://docs.fastlane.tools/actions/gradle/
gradle(
task: "clean",
project_dir: "android",
)
update_sentry_release_and_dist
end
after_all do |lane, options|
update_sentry_release_and_dist(reset: true)
sh "rm -rf .env"
notify_slack(platform: :android, track: lane)
end
lane :generate_apk do |options|
track = options[:track]
# Get keystore password from keystore.properties file
ANDROID_KEYSTORE_PASS = sh("grep -E 'storePassword' ../#{ANDROID_KEYSTORE_PROPERTIES_FILE} | cut -d'=' -f2").strip
ANDROID_KEYSTORE_ALIAS = sh("grep -E 'keyAlias' ../#{ANDROID_KEYSTORE_PROPERTIES_FILE} | cut -d'=' -f2").strip
AAB_DIR = track == :alpha ? AAB_DIR_STAGING : AAB_DIR_PRODUCTION
AAB_FILE_NAME = track == :alpha ? "app-staging-release.aab" : "app-production-release.aab"
APK_FILE_NAME = track == :alpha ? "app-staging-release.apk" : "app-production-release.apk"
bundletool(
ks_path: ANDROID_KEYSTORE_FILE,
ks_password: ANDROID_KEYSTORE_PASS,
ks_key_alias: ANDROID_KEYSTORE_ALIAS,
ks_key_alias_password: ANDROID_KEYSTORE_PASS,
bundletool_version: "1.15.2",
aab_path: "#{AAB_DIR}/#{AAB_FILE_NAME}",
apk_output_path: "#{AAB_DIR}/#{APK_FILE_NAME}",
verbose: true
)
end
lane :compile do |options|
track = options[:track]
begin
if track == :alpha
sh "git checkout ../#{ANDROID_BADGE_DIR}/"
generate_badge(platform: :android)
end
create_sourcemap(platform: :android)
# For customization, see: https://docs.fastlane.tools/actions/gradle/
gradle(
task: "bundle",
build_type: "Release",
project_dir: "android",
flavor: track == :alpha ? "staging" : "production",
properties: {
customVersionCode: BUILD_NUMBER_NEXT,
customVersionName: VERSION_NUMBER_NEXT,
},
)
ensure
if track == :alpha
sh "git checkout ../#{ANDROID_BADGE_DIR}/"
end
end
end
lane :upload do |options|
track = options[:track]
AAB_FILE_NAME = track == :alpha ? "app-staging-release.aab" : "app-production-release.aab"
# For customization, see: https://docs.fastlane.tools/actions/supply/
supply(
# track: track == :alpha ? "internal" : track.to_s,
track: "internal",
version_name: "Version #{VERSION_NUMBER_NEXT}+build#{BUILD_NUMBER_NEXT}",
aab_paths: ["#{ARTIFACTS_DIR}/#{VERSION_NUMBER_NEXT}/#{BUILD_NUMBER_NEXT}/android/#{track.to_s}/#{AAB_FILE_NAME}"],
release_status: track == :alpha ? "draft" : "completed"
)
end
lane :alpha do |options|
track = :alpha
compile(track: track)
generate_apk(track: track)
copy_build_to_artifacts(platform: :android, track: track)
upload(track: track)
upload_symbols(platform: :android, track: track)
end
lane :beta do |options|
track = :beta
compile(track: track)
copy_build_to_artifacts(platform: :android, track: track)
upload(track: track)
upload_symbols(platform: :android, track: track)
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment