mirror of
https://github.com/selfxyz/self.git
synced 2026-01-10 15:18:18 -05:00
* code rabbit feedback for staging auto deploy logic * fix jest test * cr feedback * workflow fixes * fix tests * restore cache
432 lines
16 KiB
Ruby
432 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"
|
|
|
|
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
|