$appName = File.basename(Dir['../*.xcworkspace'].first, '.*') require_relative 'fastlane/touchlane/lib/touchlane' private_lane :installDependencies do |options| podsReposPath = File.expand_path "~/.cocoapods/repos/master/" lockFilePath = "#{podsReposPath}/.git/index.lock" # check if .lock file exists in pod repos - then remove all master repo if File.exists? lockFilePath sh("rm -rf #{podsReposPath}") end cocoapods( try_repo_update_on_error: true ) end private_lane :uploadToFirebase do |options| releaseNotesFile = "release-notes.txt" sh("touch ../#{releaseNotesFile}") app_target_folder_name = options[:appName] || $appName configuration_type = Touchlane::ConfigurationType.from_type(options[:type]) gsp_plist_path = get_google_services_plist_path(app_target_folder_name, configuration_type) google_app_id = get_info_plist_value(path: gsp_plist_path, key: "GOOGLE_APP_ID") firebase_app_distibution_groups_path = File.expand_path "../firebase_app_distribution_groups" # Select groups_file or groups parameter depending on groups file existence if File.exists? firebase_app_distibution_groups_path firebase_app_distribution( app: google_app_id, ipa_path: options[:ipa_path], groups_file: firebase_app_distibution_groups_path, release_notes_file: releaseNotesFile ) else firebase_app_distribution( app: google_app_id, ipa_path: options[:ipa_path], groups: "touch-instinct", release_notes_file: releaseNotesFile ) end end def upload_to_app_store_using_options(options, submit_for_review = false) upload_to_app_store( username: options[:username] || options[:apple_id], api_key_path: options[:api_key_path], api_key: options[:api_key], ipa: options[:ipa_path], build_number: options[:buildNumber], skip_binary_upload: options[:ipa_path].nil?, skip_screenshots: true, force: true, # skip metainfo prompt submit_for_review: submit_for_review, submission_information: options[:submission_information], skip_metadata: true, team_id: options[:itc_team_id], dev_portal_team_id: options[:team_id], precheck_include_in_app_purchases: false ) end private_lane :addShield do |options| buildNumber = options[:buildNumber] buildDescription = options[:lane_name] # EnterpriseCustomerDev1WithoutSSLPinningRelease .split(/(?=[A-Z])/) # -> ["Enterprise", "Customer", "Dev1", "Without", "S", "S", "L", "Pinning", "Release"] .map { |v| v.gsub(/[[:lower:]]+/, "") }[1..2] # -> ["E", "C", "D1", "W", "S", "S", "L", "P", "R"] -> ["C", "D1"] .join # -> "CD1" begin add_badge( shield: "#{buildDescription}-#{buildNumber}-green", no_badge: true ) rescue => error UI.error(error) end end private_lane :buildConfiguration do |options| options[:appName] = options[:appName] || $appName lane_name = options[:lane_name] || lane_context[SharedValues::LANE_NAME] options[:scheme] = options[:scheme] || options[:appName] options[:lane_name] = lane_name ipa_name = "#{options[:appName]}.ipa" options[:output_name] = ipa_name options[:ipa_path] = "./#{ipa_name}" options[:dsym_path] = "./#{options[:appName]}.app.dSYM.zip" options[:xcodeproj_path] = options[:xcodeproj_path] || "../#{options[:appName]}.xcodeproj" options[:workspace] = options[:workspace] || File.expand_path("../#{options[:appName]}.xcworkspace") configuration_type = Touchlane::ConfigurationType.from_lane_name(lane_name) options = fill_up_options_using_configuration_type(options, configuration_type, true) generate_xcodeproj_if_needed(options) openKeychain(options) if !options[:buildNumber].nil? increment_build_number( build_number: options[:buildNumber] ) end installDependencies(options) run_code_generation_phase_if_needed(options) if !(options[:uploadToFabric] || options[:uploadToAppStore]) options[:skip_package_ipa] = true install_signing_identities(options) buildArchive(options) # check build failures and static analysis end if options[:uploadToFabric] install_signing_identities(options) addShield(options) buildArchive(options) uploadToFirebase(options) end if options[:uploadToAppStore] options[:include_symbols] = options[:include_symbols].nil? ? true : options[:include_symbols] install_signing_identities(options) buildArchive(options) upload_to_app_store_using_options(options, false) end upload_symbols_to_crashlytics( gsp_path: get_google_services_plist_path(options[:appName], configuration_type) ) end private_lane :buildArchive do |options| require 'json' icloudEnvironment = options[:iCloudContainerEnvironment] || "" exportOptions = icloudEnvironment.to_s.empty? ? {} : {iCloudContainerEnvironment: icloudEnvironment} lane_name = options[:lane_name] configuration = options[:configuration] xcodeproj_path = options[:xcodeproj_path] xcodes(select_for_current_build_only: true) if configuration != "AppStore" # AppStore uses xcconfig choosen in Xcode set_xcconfig_for_configuration_of_project(lane_name, configuration, xcodeproj_path) end gym( clean: true, workspace: options[:workspace], scheme: options[:scheme], archive_path: "./#{$appName}.xcarchive", buildlog_path: "./", output_name: options[:output_name], configuration: configuration, export_method: options[:export_method], export_options: exportOptions, skip_package_ipa: options[:skip_package_ipa], include_symbols: options[:include_symbols] || false ) end lane :SubmitForReview do |options| configuration_type = Touchlane::ConfigurationType.from_type("appstore") options = fill_up_options_using_configuration_type(options, configuration_type, false) upload_to_app_store_using_options(options, true) end lane :InstallDevelopmentSigningIdentities do |options| configuration_type = Touchlane::ConfigurationType.from_type("development") options = fill_up_options_using_configuration_type(options, configuration_type) install_signing_identities(options) end lane :RefreshProfiles do |options| type = options[:type] || "development" configuration_type = Touchlane::ConfigurationType.from_type(type) options = fill_up_options_using_configuration_type(options, configuration_type) refresh_profiles(options) end lane :ReplaceDevelopmentCertificate do |options| configuration_type = Touchlane::ConfigurationType.from_type("development") options = fill_up_options_using_configuration_type(options, configuration_type, true) replace_development_certificate(options) end lane :SyncAppStoreIdentities do |options| configuration_type = Touchlane::ConfigurationType.from_type("appstore") options = fill_up_options_using_configuration_type(options, configuration_type, true) options[:readonly] = false sync_signing_identities(options) end lane :ManuallyUpdateCodeSigning do |options| manually_update_code_signing(get_default_options.merge(options)) end private_lane :openKeychain do |options| if is_ci? # workaround to avoid duplication problem # https://apple.stackexchange.com/questions/350633/multiple-duplicate-keychain-dbs-that-dont-get-cleaned-up keychain_path = File.expand_path("~/Library/Keychains/#{options[:keychain_name]}") keychain_exists = File.exist?("#{keychain_path}-db") || File.exist?(keychain_path) create_keychain( name: options[:keychain_name], password: options[:keychain_password], unlock: true, timeout: 0, add_to_search_list: !keychain_exists ) else unlock_keychain( path: options[:keychain_name], password: options[:keychain_password] ) end end def get_default_options { :git_url => get_signing_identities_path(), :signing_identities_path => get_signing_identities_path(), :storage_mode => Touchlane::LocalStorage::STORAGE_TYPE } end def get_signing_identities_path File.expand_path "../EncryptedSigningIdentities" end def fill_up_options_using_configuration_type(options, configuration_type, keychain_password_required = false) configuration = get_configuration_for_type(configuration_type.type) api_key_path = File.expand_path "../fastlane/#{configuration_type.prefix}_api_key.json" is_api_key_file_exists = File.exists?(api_key_path) # default_options required to be empty due to the possibility of skipping the configuration type check below default_options = get_default_options # Check whether configuration type is required to configure one of api key parameters or not if configuration_type.is_app_store || configuration_type.is_development # Check whether API key JSON file exists or not if is_api_key_file_exists # If exists then fill in all required information through api_key_path parameter # and set a value to an options` parameter respectively default_options[:api_key_path] = api_key_path else # If doesn't exist then build api_key parameter through app_store_connect_api_key action # and set a value to an options` parameter respectively also default_options[:api_key] = get_app_store_connect_api_key() end end default_options .merge(configuration.to_options) .merge(get_keychain_options(options, keychain_password_required)) .merge(options) end def get_app_store_connect_api_key() require 'json' api_key_parameters = JSON.parse(ENV['API_KEY_JSON']) return app_store_connect_api_key( key_id: api_key_parameters['key_id'], issuer_id: api_key_parameters['issuer_id'], key_content: api_key_parameters['key'], duration: api_key_parameters['duration'], in_house: api_key_parameters['in_house'] ) end def get_keychain_options(options, keychain_password_required = false) keychain_name = options[:keychain_name] keychain_password = options[:keychain_password] if is_ci? keychain_name = keychain_name || "ci.keychain" keychain_password = keychain_password || "" elsif keychain_password_required && keychain_password.nil? keychain_password = prompt( text: "Please enter your keychain password (account password): ", secure_text: true ) end return {:keychain_name => keychain_name, :keychain_password => keychain_password} end def get_configuration_for_type(type) config_path = File.expand_path "configurations.yaml" configuration = Touchlane::Configuration.from_file(config_path, type) end def get_google_services_plist_path(app_target_folder_name, configuration_type) File.expand_path "../#{app_target_folder_name}/Resources/GoogleService-Info.plist" end def set_xcconfig_for_configuration_of_project(lane_name, configuration, xcodeproj_path) require 'xcodeproj' project = Xcodeproj::Project.open(xcodeproj_path) target_to_modify_selector = lambda do |t| supported_product_types = [ Xcodeproj::Constants::PRODUCT_TYPE_UTI[:application], Xcodeproj::Constants::PRODUCT_TYPE_UTI[:app_extension], Xcodeproj::Constants::PRODUCT_TYPE_UTI[:framework] ] return !t.test_target_type? && supported_product_types.include?(t.product_type) end application_targets = project.native_targets.select(&target_to_modify_selector) application_targets.each do |target| config_name = target.name + lane_name build_configuration_reference = project.files.select { |f| f.path.start_with?(config_name) }.first if !build_configuration_reference.nil? # target has custom xcconfig build_configuration = target.build_configuration_list[configuration] build_configuration.base_configuration_reference = build_configuration_reference end end project.save() end def generate_xcodeproj_if_needed(options) project_yml_path = File.expand_path "../project.yml" if !File.exists?(options[:xcodeproj_path]) && File.exists?(project_yml_path) xcodegen( spec: project_yml_path ) end end # Build phases def run_code_generation_phase_if_needed(options) code_generation_script_path = File.expand_path "../.githooks/scripts/CodeGen.sh" if File.exists? code_generation_script_path sh(code_generation_script_path, options[:workspace]) end end