Files
self/app/fastlane/Fastfile
Justin Hernandez ccb9e148be App polish for 2.9 rd1 (#1359)
* add useSafeBottomPadding

* add bottom padding to dev screen

* use safe bottom padding

* skip uploading if building android bundle locally

* fix tests

* cache fix script

* clean up country picker, fix font color

* sort package jsons, add watcher for mobile sdk

* formatting

* only bump versions for successfull builds

* move all css

* cleaner script

* kill watchers before starting new one
2025-11-07 10:26:08 -08:00

440 lines
16 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 (DEPRECATED)"
lane :sync_version do
UI.error("⛔ This lane is deprecated!")
UI.error("Version management is now centralized in CI.")
UI.error("Use: node scripts/version-manager.cjs apply <version> <ios> <android>")
UI.user_error!("sync_version lane is deprecated - use version-manager.cjs instead")
end
desc "Push a new build to TestFlight Internal Testing"
lane :internal_test do |options|
test_mode = options[:test_mode] == true || options[:test_mode] == "true"
result = prepare_ios_build(prod_release: false)
if test_mode
UI.important("🧪 TEST MODE: Skipping TestFlight upload")
UI.success("✅ Build completed successfully!")
UI.message("📦 IPA path: #{result[:ipa_path]}")
else
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]
end
# Update deployment timestamp in version.json
if result[:should_upload]
Fastlane::Helpers.update_deployment_timestamp("ios")
end
end
desc "Deploy iOS app with automatic version management"
lane :deploy_auto do |options|
deployment_track = options[:deployment_track] || "internal"
version_bump = options[:version_bump] || "build"
test_mode = options[:test_mode] == true || options[:test_mode] == "true"
UI.message("🚀 Starting iOS deployment")
UI.message(" Track: #{deployment_track}")
UI.message(" Version bump: #{version_bump}")
UI.message(" Test mode: #{test_mode}")
# In CI, version should already be set by version-manager.cjs
# Verify it matches expected values
if is_ci && version_bump == "skip"
Fastlane::Helpers.verify_ci_version_match
end
# Prepare and build (no version bumping inside)
result = prepare_ios_build(prod_release: deployment_track == "production", version_bump: version_bump)
# Handle deployment based on track
if test_mode
UI.important("🧪 TEST MODE: Skipping App Store upload")
UI.success("✅ Build completed successfully!")
elsif deployment_track == "internal"
upload_to_testflight(
api_key: result[:api_key],
distribute_external: false,
skip_waiting_for_build_processing: false,
) if result[:should_upload]
elsif deployment_track == "production"
# For production, upload to TestFlight first, then promote
upload_to_testflight(
api_key: result[:api_key],
distribute_external: false,
skip_waiting_for_build_processing: true,
) if result[:should_upload]
# TODO: Add app store submission when ready
UI.important("⚠️ Production deployment uploaded to TestFlight. Manual App Store submission required.")
end
# Update deployment info
if result[:should_upload] && !test_mode
Fastlane::Helpers.update_deployment_timestamp("ios")
end
end
private_lane :prepare_ios_build do |options|
version_bump = options[:version_bump] || "build"
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)
# Read build number from version.json (already set by CI or local version-manager.cjs)
build_number = Fastlane::Helpers.get_ios_build_number
UI.message("📦 Using iOS build number: #{build_number}")
# Update Xcode project with build number
increment_build_number(
build_number: build_number,
xcodeproj: "ios/#{ENV["IOS_PROJECT_NAME"]}.xcodeproj",
)
# Verify build number is higher than TestFlight
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,
# setting to false for now because of NFCPassportReader locally we use ssh to install but the
# workflow uses https so the Podfile.lock is different
deployment: false,
)
ipa_path = build_app({
workspace: "#{workspace_path}",
scheme: PROJECT_SCHEME,
export_method: "app-store",
output_directory: "ios/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 (DEPRECATED)"
lane :sync_version do
UI.error("⛔ This lane is deprecated!")
UI.error("Version management is now centralized in CI.")
UI.error("Use: node scripts/version-manager.cjs apply <version> <ios> <android>")
UI.user_error!("sync_version lane is deprecated - use version-manager.cjs instead")
end
desc "Push a new build to Google Play Internal Testing"
lane :internal_test do |options|
upload_android_build(track: "internal", test_mode: options[:test_mode])
end
desc "Push a new build to Google Play Store"
lane :deploy do
upload_android_build(track: "production")
end
desc "Build Android app without uploading"
lane :build_only do |options|
deployment_track = options[:deployment_track] || "internal"
version_bump = options[:version_bump] || "build"
UI.message("🔨 Building Android app (build only)")
UI.message(" Track: #{deployment_track}")
UI.message(" Version bump: #{version_bump}")
upload_android_build(options.merge(skip_upload: true))
end
desc "Deploy Android app with automatic version management"
lane :deploy_auto do |options|
deployment_track = options[:deployment_track] || "internal"
version_bump = options[:version_bump] || "build"
test_mode = options[:test_mode] == true || options[:test_mode] == "true"
UI.message("🚀 Starting Android deployment")
UI.message(" Track: #{deployment_track}")
UI.message(" Version bump: #{version_bump}")
UI.message(" Test mode: #{test_mode}")
# In CI, version should already be set by version-manager.cjs
# Verify it matches expected values
if is_ci && version_bump == "skip"
Fastlane::Helpers.verify_ci_version_match
end
# Map deployment track to Play Store track
play_store_track = deployment_track == "production" ? "production" : "internal"
# Build and deploy
upload_android_build(track: play_store_track, test_mode: test_mode, deployment_track: deployment_track, version_bump: version_bump)
end
private_lane :upload_android_build do |options|
test_mode = options[:test_mode] == true || options[:test_mode] == "true"
skip_upload = options[:skip_upload] == true || options[:skip_upload] == "true"
version_bump = options[:version_bump] || "build"
# Automatically skip uploads in local development (no local permissions for Play Store)
# Uploads must be done by CI/CD machines with proper authentication
if local_development && !skip_upload
skip_upload = true
UI.important("🏠 LOCAL DEVELOPMENT: Automatically skipping Play Store upload")
UI.important(" Uploads require CI/CD machine permissions and will be handled automatically")
end
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",
]
# Only require JSON key path when not running in CI (local development)
required_env_vars << "ANDROID_PLAY_STORE_JSON_KEY_PATH" if local_development
Fastlane::Helpers.verify_env_vars(required_env_vars)
# Read version code from version.json (already set by CI or local version-manager.cjs)
version_code = Fastlane::Helpers.get_android_build_number
UI.message("📦 Using Android build number: #{version_code}")
# Update build.gradle with version code
increment_version_code(
version_code: version_code,
gradle_file_path: android_gradle_file_path.gsub("../", ""),
)
# 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 JSON key only in local development; CI uses Workload Identity Federation (ADC)
if local_development
validate_play_store_json_key(
json_key: ENV["ANDROID_PLAY_STORE_JSON_KEY_PATH"],
)
end
Fastlane::Helpers.with_retry(max_retries: 3, delay: 10) do
gradle(
task: "clean bundleRelease --stacktrace --info",
project_dir: "android/",
properties: {
"MYAPP_UPLOAD_STORE_FILE" => ENV["ANDROID_KEYSTORE_PATH"],
"MYAPP_UPLOAD_STORE_PASSWORD" => ENV["ANDROID_KEYSTORE_PASSWORD"],
"MYAPP_UPLOAD_KEY_ALIAS" => ENV["ANDROID_KEY_ALIAS"],
"MYAPP_UPLOAD_KEY_PASSWORD" => ENV["ANDROID_KEY_PASSWORD"] == "EMPTY" ? "" : ENV["ANDROID_KEY_PASSWORD"],
},
)
end
if test_mode || skip_upload
if skip_upload
UI.important("🔨 BUILD ONLY: Skipping Play Store upload")
else
UI.important("🧪 TEST MODE: Skipping Play Store upload")
end
UI.success("✅ Build completed successfully!")
UI.message("📦 AAB path: #{android_aab_path}")
else
if should_upload
begin
upload_options = {
track: options[:track],
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,
}
# In local development, use the JSON key file; in CI rely on ADC
if local_development
upload_options[:json_key] = ENV["ANDROID_PLAY_STORE_JSON_KEY_PATH"]
else
# In CI, try to use ADC credentials file directly
adc_creds_path = ENV["GOOGLE_APPLICATION_CREDENTIALS"]
if adc_creds_path && File.exist?(adc_creds_path)
UI.message("🔑 Using ADC credentials file: #{adc_creds_path}")
begin
# Try passing the credentials file content as json_key_data
creds_content = File.read(adc_creds_path)
upload_options[:json_key_data] = creds_content
rescue => e
UI.error("Failed to read ADC credentials: #{e.message}")
# Fallback: let supply try to use ADC automatically
UI.message("🔄 Falling back to automatic ADC detection")
end
else
UI.error("❌ ADC credentials not found at: #{adc_creds_path}")
end
end
upload_to_play_store(upload_options)
rescue => e
if e.message.include?("forbidden") || e.message.include?("403") || e.message.include?("insufficientPermissions")
UI.error("❌ Play Store upload failed: Insufficient permissions")
UI.error("Please fix permissions in Google Play Console")
UI.important("Build saved at: #{android_aab_path}")
else
# Re-raise if it's a different error
raise e
end
end
else
UI.message("Skipping Play Store upload (should_upload: false)")
end
end
# Update deployment timestamp in version.json
if should_upload
Fastlane::Helpers.update_deployment_timestamp("android")
end
end
end