Created
August 9, 2023 09:56
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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