Files
self/app/fastlane/Fastfile
Justin Hernandez 9aa4faad13 Address minor mobile deployment bugs (#745)
* feat: improve deployment tooling

* cr feedback

* for temp testing

* clean build artifacts after deploy

* add deploy source

* uncomment ios commands

* Add tests for minor deployment fixes (#750)

* Add test coverage for deployment scripts and Fastfile

* format

* increase github check to 5 minutes
2025-07-06 18:21:46 -07:00

288 lines
9.7 KiB
Ruby

# This file contains the fastlane.tools configuration
# You can find the documentation at https://docs.fastlane.tools
#
# For a list of all available actions, check out
# https://docs.fastlane.tools/actions
#
opt_out_usage
require "bundler/setup"
require "base64"
require "xcodeproj"
require_relative "helpers"
# load secrets before project configuration
Fastlane::Helpers.dev_load_dotenv_secrets
is_ci = Fastlane::Helpers.is_ci_environment?
local_development = !is_ci
# checks after calling Dotenv.load
android_has_permissions = false
# Project configuration
PROJECT_NAME = ENV["IOS_PROJECT_NAME"]
APP_NAME = ENV["IOS_PROJECT_NAME"] || begin
app_json_path = File.expand_path("../app.json", __dir__)
if File.exist?(app_json_path)
app_config = JSON.parse(File.read(app_json_path))
app_config["displayName"] if app_config.is_a?(Hash)
end
rescue JSON::ParserError, Errno::ENOENT
UI.important("Could not read app.json or invalid JSON format, using default app name")
nil
end || "MobileApp"
PROJECT_SCHEME = ENV["IOS_PROJECT_SCHEME"]
SIGNING_CERTIFICATE = ENV["IOS_SIGNING_CERTIFICATE"]
# Environment setup
package_version = JSON.parse(File.read("../package.json"))["version"]
# most of these values are for local development
android_aab_path = "../android/app/build/outputs/bundle/release/app-release.aab"
android_gradle_file_path = "../android/app/build.gradle"
android_keystore_path = "../android/app/upload-keystore.jks"
android_play_store_json_key_path = "../android/app/play-store-key.json"
ios_connect_api_key_path = "../ios/certs/connect_api_key.p8"
ios_provisioning_profile_directory = "~/Library/MobileDevice/Provisioning\ Profiles"
ios_xcode_profile_path = "../ios/#{PROJECT_NAME}.xcodeproj"
default_platform(:ios)
platform :ios do
desc "Sync ios version"
lane :sync_version do
increment_version_number(
xcodeproj: "ios/#{PROJECT_NAME}.xcodeproj",
version_number: package_version,
)
end
desc "Push a new build to TestFlight Internal Testing"
lane :internal_test do
result = prepare_ios_build(prod_release: false)
upload_to_testflight(
api_key: result[:api_key],
distribute_external: true,
# Only external TestFlight groups are valid here
groups: ENV["IOS_TESTFLIGHT_GROUPS"].split(","),
changelog: "",
skip_waiting_for_build_processing: false,
) if result[:should_upload]
# Notify Slack about the new build
if ENV["SLACK_CHANNEL_ID"]
deploy_source = Fastlane::Helpers.is_ci_environment? ? "GitHub Workflow" : "Local Deploy"
Fastlane::Helpers.upload_file_to_slack(
file_path: result[:ipa_path],
channel_id: ENV["SLACK_CHANNEL_ID"],
initial_comment: "🍎 iOS v#{package_version} (Build #{result[:build_number]}) deployed to TestFlight via #{deploy_source}",
title: "#{APP_NAME}-#{package_version}-#{result[:build_number]}.ipa",
)
else
UI.important("Skipping Slack notification: SLACK_CHANNEL_ID not set.")
end
end
private_lane :prepare_ios_build do |options|
if local_development
# app breaks with Xcode 16.3
xcode_select "/Applications/Xcode.app"
# Set up API key, profile, and potentially certificate for local dev
Fastlane::Helpers.ios_dev_setup_connect_api_key(ios_connect_api_key_path)
Fastlane::Helpers.ios_dev_setup_provisioning_profile(ios_provisioning_profile_directory)
Fastlane::Helpers.ios_dev_setup_certificate
else
# we need this for building ios apps in CI
# else build will hang on "[CP] Embed Pods Frameworks"
setup_ci(
keychain_name: "build.keychain",
)
end
required_env_vars = [
"IOS_APP_IDENTIFIER",
"IOS_CONNECT_API_KEY_BASE64",
"IOS_CONNECT_API_KEY_PATH",
"IOS_CONNECT_ISSUER_ID",
"IOS_CONNECT_KEY_ID",
"IOS_PROJECT_NAME",
"IOS_PROJECT_SCHEME",
"IOS_PROV_PROFILE_NAME",
"IOS_PROV_PROFILE_PATH",
"IOS_TEAM_ID",
"IOS_TEAM_NAME",
]
target_platform = options[:prod_release] ? "App Store" : "TestFlight"
should_upload = Fastlane::Helpers.should_upload_app(target_platform)
workspace_path = File.expand_path("../ios/#{PROJECT_NAME}.xcworkspace", Dir.pwd)
ios_signing_certificate_name = "iPhone Distribution: #{ENV["IOS_TEAM_NAME"]} (#{ENV["IOS_TEAM_ID"]})"
Fastlane::Helpers.verify_env_vars(required_env_vars)
# Get current build number without auto-incrementing
project = Xcodeproj::Project.open(ios_xcode_profile_path)
target = project.targets.first
config = target.build_configurations.first
build_number = config.build_settings["CURRENT_PROJECT_VERSION"]
# Verify build number is higher than TestFlight (but don't auto-increment)
Fastlane::Helpers.ios_verify_app_store_build_number(ios_xcode_profile_path)
Fastlane::Helpers.ios_verify_provisioning_profile
api_key = app_store_connect_api_key(
key_id: ENV["IOS_CONNECT_KEY_ID"],
issuer_id: ENV["IOS_CONNECT_ISSUER_ID"],
key_filepath: ENV["IOS_CONNECT_API_KEY_PATH"],
in_house: false,
)
# Update project to use manual code signing
update_code_signing_settings(
use_automatic_signing: false,
path: "ios/#{PROJECT_NAME}.xcodeproj",
team_id: ENV["IOS_TEAM_ID"],
targets: [PROJECT_NAME],
code_sign_identity: ios_signing_certificate_name,
profile_name: ENV["IOS_PROV_PROFILE_NAME"],
bundle_identifier: ENV["IOS_APP_IDENTIFIER"],
build_configurations: ["Release"],
)
clear_derived_data
# Print final build settings before archiving
sh "xcodebuild -showBuildSettings -workspace #{workspace_path} " \
"-scheme #{PROJECT_SCHEME} -configuration Release " \
"| grep 'CODE_SIGN_STYLE\|PROVISIONING_PROFILE_SPECIFIER\|CODE_SIGN_IDENTITY\|DEVELOPMENT_TEAM' || true"
cocoapods(
podfile: "ios/Podfile",
clean_install: true,
deployment: true,
)
ipa_path = build_app({
workspace: "#{workspace_path}",
scheme: PROJECT_SCHEME,
export_method: "app-store",
output_directory: "build",
clean: true,
export_options: {
method: "app-store",
signingStyle: "manual",
provisioningProfiles: {
ENV["IOS_APP_IDENTIFIER"] => ENV["IOS_PROV_PROFILE_NAME"],
},
signingCertificate: ios_signing_certificate_name,
teamID: ENV["IOS_TEAM_ID"],
},
})
{
api_key: api_key,
build_number: build_number,
ipa_path: ipa_path,
should_upload: should_upload,
}
end
end
platform :android do
desc "Sync android version"
lane :sync_version do
android_set_version_name(
version_name: package_version,
gradle_file: android_gradle_file_path.gsub("../", ""),
)
end
desc "Push a new build to Google Play Internal Testing"
lane :internal_test do
upload_android_build(track: "internal")
end
desc "Push a new build to Google Play Store"
lane :deploy do
upload_android_build(track: "production")
end
private_lane :upload_android_build do |options|
if local_development
if ENV["ANDROID_KEYSTORE_PATH"].nil?
ENV["ANDROID_KEYSTORE_PATH"] = Fastlane::Helpers.android_create_keystore(android_keystore_path)
end
if ENV["ANDROID_PLAY_STORE_JSON_KEY_PATH"].nil?
ENV["ANDROID_PLAY_STORE_JSON_KEY_PATH"] = Fastlane::Helpers.android_create_play_store_key(android_play_store_json_key_path)
end
end
required_env_vars = [
"ANDROID_KEYSTORE",
"ANDROID_KEYSTORE_PASSWORD",
"ANDROID_KEYSTORE_PATH",
"ANDROID_KEY_ALIAS",
"ANDROID_KEY_PASSWORD",
"ANDROID_PACKAGE_NAME",
"ANDROID_PLAY_STORE_JSON_KEY_PATH",
]
Fastlane::Helpers.verify_env_vars(required_env_vars)
# Get current version code without auto-incrementing
content = File.read(android_gradle_file_path)
match = content.match(/versionCode\s+(\d+)/)
version_code = match ? match[1].to_i : 1
# TODO: uncomment when we have the permissions to run this action
# Fastlane::Helpers.android_verify_version_code(android_gradle_file_path)
target_platform = options[:track] == "production" ? "Google Play" : "Internal Testing"
should_upload = Fastlane::Helpers.should_upload_app(target_platform)
validate_play_store_json_key(
json_key: ENV["ANDROID_PLAY_STORE_JSON_KEY_PATH"],
)
Fastlane::Helpers.with_retry(max_retries: 3, delay: 10) do
gradle(
task: "clean bundleRelease --stacktrace --info",
project_dir: "android/",
properties: {
"android.injected.signing.store.file" => ENV["ANDROID_KEYSTORE_PATH"],
"android.injected.signing.store.password" => ENV["ANDROID_KEYSTORE_PASSWORD"],
"android.injected.signing.key.alias" => ENV["ANDROID_KEY_ALIAS"],
"android.injected.signing.key.password" => ENV["ANDROID_KEY_PASSWORD"],
},
)
end
upload_to_play_store(
track: options[:track],
json_key: ENV["ANDROID_PLAY_STORE_JSON_KEY_PATH"],
package_name: ENV["ANDROID_PACKAGE_NAME"],
skip_upload_changelogs: true,
skip_upload_images: true,
skip_upload_screenshots: true,
track_promote_release_status: "completed",
aab: android_aab_path,
) if should_upload && android_has_permissions
# Notify Slack about the new build
if ENV["SLACK_CHANNEL_ID"]
deploy_source = Fastlane::Helpers.is_ci_environment? ? "GitHub Workflow" : "Local Deploy"
Fastlane::Helpers.upload_file_to_slack(
file_path: android_aab_path,
channel_id: ENV["SLACK_CHANNEL_ID"],
initial_comment: "🤖 Android v#{package_version} (Build #{version_code}) deployed to #{target_platform} via #{deploy_source}",
title: "#{APP_NAME}-#{package_version}-#{version_code}.aab",
)
else
UI.important("Skipping Slack notification: SLACK_CHANNEL_ID not set.")
end
end
end