class FastlaneCore::ItunesTransporter

Constants

TWO_FACTOR_ENV_VARIABLE
TWO_STEP_HOST_PREFIX

Public Class Methods

hide_transporter_output() click to toggle source

This will be called from the Deliverfile, and disables the logging of the transporter output

# File fastlane_core/lib/fastlane_core/itunes_transporter.rb, line 662
def self.hide_transporter_output
  @hide_transporter_output = !FastlaneCore::Globals.verbose?
end
hide_transporter_output?() click to toggle source
# File fastlane_core/lib/fastlane_core/itunes_transporter.rb, line 666
def self.hide_transporter_output?
  @hide_transporter_output
end
new(user = nil, password = nil, use_shell_script = false, provider_short_name = nil, jwt = nil, altool_compatible_command: false, api_key: nil) click to toggle source

Returns a new instance of the iTunesTransporter. If no username or password given, it will be taken from the #{CredentialsManager::AccountManager} @param use_shell_script if true, forces use of the iTMSTransporter shell script.

if false, allows a direct call to the iTMSTransporter Java app (preferred).
see: https://github.com/fastlane/fastlane/pull/4003

@param provider_short_name The provider short name to be given to the iTMSTransporter to identify the

correct team for this work. The provider short name is usually your Developer
Portal team ID, but in certain cases it is different!
see: https://github.com/fastlane/fastlane/issues/1524#issuecomment-196370628
for more information about how to use the iTMSTransporter to list your provider
short names
# File fastlane_core/lib/fastlane_core/itunes_transporter.rb, line 682
def initialize(user = nil, password = nil, use_shell_script = false, provider_short_name = nil, jwt = nil, altool_compatible_command: false, api_key: nil)
  # Xcode 6.x doesn't have the same iTMSTransporter Java setup as later Xcode versions, so
  # we can't default to using the newer direct Java invocation strategy for those versions.
  use_shell_script ||= Helper.is_mac? && Helper.xcode_version.start_with?('6.')
  use_shell_script ||= Helper.windows?
  use_shell_script ||= Feature.enabled?('FASTLANE_ITUNES_TRANSPORTER_USE_SHELL_SCRIPT')

  if jwt.to_s.empty?
    @user = user
    @password = password || load_password_for_transporter
  end

  @jwt = jwt
  @api_key = api_key

  if should_use_altool?(altool_compatible_command, use_shell_script)
    UI.verbose("Using altool as transporter.")
    @transporter_executor = AltoolTransporterExecutor.new
  else
    UI.verbose("Using iTMSTransporter as transporter.")
    @transporter_executor = use_shell_script ? ShellScriptTransporterExecutor.new : JavaTransporterExecutor.new
  end

  @provider_short_name = provider_short_name
end

Public Instance Methods

displayable_errors() click to toggle source
# File fastlane_core/lib/fastlane_core/itunes_transporter.rb, line 873
def displayable_errors
  @transporter_executor.displayable_errors
end
download(app_id, dir = nil) click to toggle source

Downloads the latest version of the app metadata package from iTC. @param app_id [Integer] The unique App ID @param dir [String] the path in which the package file should be stored @return (Bool) True if everything worked fine @raise [Deliver::TransporterTransferError] when something went wrong

when transferring
# File fastlane_core/lib/fastlane_core/itunes_transporter.rb, line 714
def download(app_id, dir = nil)
  dir ||= "/tmp"

  password_placeholder = @jwt.nil? ? 'YourPassword' : nil
  jwt_placeholder = @jwt.nil? ? nil : 'YourJWT'

  UI.message("Going to download app metadata from App Store Connect")
  command = @transporter_executor.build_download_command(@user, @password, app_id, dir, @provider_short_name, @jwt)
  UI.verbose(@transporter_executor.build_download_command(@user, password_placeholder, app_id, dir, @provider_short_name, jwt_placeholder))

  begin
    result = @transporter_executor.execute(command, ItunesTransporter.hide_transporter_output?)
  rescue TransporterRequiresApplicationSpecificPasswordError => ex
    handle_two_step_failure(ex)
    return download(app_id, dir)
  end

  return result if Helper.test?

  itmsp_path = File.join(dir, "#{app_id}.itmsp")
  successful = result && File.directory?(itmsp_path)

  if successful
    UI.success("✅ Successfully downloaded the latest package from App Store Connect to #{itmsp_path}")
  else
    handle_error(@password)
  end

  successful
end
provider_ids() click to toggle source
# File fastlane_core/lib/fastlane_core/itunes_transporter.rb, line 877
def provider_ids
  password_placeholder = @jwt.nil? ? 'YourPassword' : nil
  jwt_placeholder = @jwt.nil? ? nil : 'YourJWT'

  # Handle AppStore Connect API
  use_api_key = !@api_key.nil?
  api_key_placeholder = use_api_key ? { key_id: "YourKeyID", issuer_id: "YourIssuerID", key_dir: "YourTmpP8KeyDir" } : nil

  api_key = nil
  api_key = api_key_with_p8_file_path(@api_key) if use_api_key

  command = @transporter_executor.build_provider_ids_command(@user, @password, @jwt, api_key)
  UI.verbose(@transporter_executor.build_provider_ids_command(@user, password_placeholder, jwt_placeholder, api_key_placeholder))

  lines = []
  begin
    result = @transporter_executor.execute(command, ItunesTransporter.hide_transporter_output?) { |xs| lines = xs }
    return result if Helper.test?
  rescue TransporterRequiresApplicationSpecificPasswordError => ex
    handle_two_step_failure(ex)
    return provider_ids
  ensure
    if use_api_key
      FileUtils.rm_rf(api_key[:key_dir]) unless api_key.nil?
    end
  end

  @transporter_executor.parse_provider_info(lines)
end
upload(app_id = nil, dir = nil, package_path: nil, asset_path: nil, platform: nil) click to toggle source

Uploads the modified package back to App Store Connect @param app_id [Integer] The unique App ID @param dir [String] the path in which the package file is located @param package_path [String] the path to the package file (used instead of app_id and dir) @param asset_path [String] the path to the ipa/dmg/pkg file (used instead of package_path if running on macOS) @return (Bool) True if everything worked fine @raise [Deliver::TransporterTransferError] when something went wrong

when transferring
# File fastlane_core/lib/fastlane_core/itunes_transporter.rb, line 753
def upload(app_id = nil, dir = nil, package_path: nil, asset_path: nil, platform: nil)
  raise "app_id and dir are required or package_path or asset_path is required" if (app_id.nil? || dir.nil?) && package_path.nil? && asset_path.nil?

  # Transport can upload .ipa, .dmg, and .pkg files directly with -assetFile
  # However, -assetFile requires -assetDescription if Linux or Windows
  # This will give the asset directly if macOS and asset_path exists
  # otherwise it will use the .itmsp package

  force_itmsp = FastlaneCore::Env.truthy?("ITMSTRANSPORTER_FORCE_ITMS_PACKAGE_UPLOAD")
  can_use_asset_path = Helper.is_mac? && asset_path

  actual_dir = if can_use_asset_path && !force_itmsp
                 # The asset gets deleted upon completion so copying to a temp directory
                 # (with randomized filename, for multibyte-mixed filename upload fails)
                 new_file_name = "#{SecureRandom.uuid}#{File.extname(asset_path)}"
                 tmp_asset_path = File.join(Dir.tmpdir, new_file_name)
                 FileUtils.cp(asset_path, tmp_asset_path)
                 tmp_asset_path
               elsif package_path
                 package_path
               else
                 File.join(dir, "#{app_id}.itmsp")
               end

  UI.message("Going to upload updated app to App Store Connect")
  UI.success("This might take a few minutes. Please don't interrupt the script.")

  password_placeholder = @jwt.nil? ? 'YourPassword' : nil
  jwt_placeholder = @jwt.nil? ? nil : 'YourJWT'

  # Handle AppStore Connect API
  use_api_key = !@api_key.nil?
  api_key_placeholder = use_api_key ? { key_id: "YourKeyID", issuer_id: "YourIssuerID", key_dir: "YourTmpP8KeyDir" } : nil

  api_key = nil
  api_key = api_key_with_p8_file_path(@api_key) if use_api_key

  command = @transporter_executor.build_upload_command(@user, @password, actual_dir, @provider_short_name, @jwt, platform, api_key)
  UI.verbose(@transporter_executor.build_upload_command(@user, password_placeholder, actual_dir, @provider_short_name, jwt_placeholder, platform, api_key_placeholder))

  begin
    result = @transporter_executor.execute(command, ItunesTransporter.hide_transporter_output?)
  rescue TransporterRequiresApplicationSpecificPasswordError => ex
    handle_two_step_failure(ex)
    return upload(app_id, dir, package_path: package_path, asset_path: asset_path)
  ensure
    if use_api_key
      FileUtils.rm_rf(api_key[:key_dir]) unless api_key.nil?
    end
  end

  if result
    UI.header("Successfully uploaded package to App Store Connect. It might take a few minutes until it's visible online.")

    FileUtils.rm_rf(actual_dir) unless Helper.test? # we don't need the package anymore, since the upload was successful
  else
    handle_error(@password)
  end

  return result
end
verify(app_id = nil, dir = nil, package_path: nil, asset_path: nil, platform: nil) click to toggle source

Verifies the given binary with App Store Connect @param app_id [Integer] The unique App ID @param dir [String] the path in which the package file is located @param package_path [String] the path to the package file (used instead of app_id and dir) @return (Bool) True if everything worked fine @raise [Deliver::TransporterTransferError] when something went wrong

when transferring
# File fastlane_core/lib/fastlane_core/itunes_transporter.rb, line 822
def verify(app_id = nil, dir = nil, package_path: nil, asset_path: nil, platform: nil)
  raise "app_id and dir are required or package_path or asset_path is required" if (app_id.nil? || dir.nil?) && package_path.nil? && asset_path.nil?

  force_itmsp = FastlaneCore::Env.truthy?("ITMSTRANSPORTER_FORCE_ITMS_PACKAGE_UPLOAD")
  can_use_asset_path = Helper.is_mac? && asset_path

  actual_dir = if can_use_asset_path && !force_itmsp
                 # The asset gets deleted upon completion so copying to a temp directory
                 # (with randomized filename, for multibyte-mixed filename upload fails)
                 new_file_name = "#{SecureRandom.uuid}#{File.extname(asset_path)}"
                 tmp_asset_path = File.join(Dir.tmpdir, new_file_name)
                 FileUtils.cp(asset_path, tmp_asset_path)
                 tmp_asset_path
               elsif package_path
                 package_path
               else
                 File.join(dir, "#{app_id}.itmsp")
               end

  password_placeholder = @jwt.nil? ? 'YourPassword' : nil
  jwt_placeholder = @jwt.nil? ? nil : 'YourJWT'

  # Handle AppStore Connect API
  use_api_key = !@api_key.nil?

  # Masking credentials for verbose outputs
  api_key_placeholder = use_api_key ? { key_id: "YourKeyID", issuer_id: "YourIssuerID", key_dir: "YourTmpP8KeyDir" } : nil

  api_key = api_key_with_p8_file_path(@api_key) if use_api_key

  command = @transporter_executor.build_verify_command(@user, @password, actual_dir, @provider_short_name, jwt: @jwt, platform: platform, api_key: api_key)
  UI.verbose(@transporter_executor.build_verify_command(@user, password_placeholder, actual_dir, @provider_short_name, jwt: jwt_placeholder, platform: platform, api_key: api_key_placeholder))

  begin
    result = @transporter_executor.execute(command, ItunesTransporter.hide_transporter_output?)
  rescue TransporterRequiresApplicationSpecificPasswordError => ex
    handle_two_step_failure(ex)
    return verify(app_id, dir, package_path: package_path)
  end

  if result
    UI.header("Successfully verified package on App Store Connect")

    FileUtils.rm_rf(actual_dir) unless Helper.test? # we don't need the package anymore, since the upload was successful
  else
    handle_error(@password)
  end

  return result
end

Private Instance Methods

api_key_with_p8_file_path(original_api_key) click to toggle source

Create .p8 file from api_key and provide api key info which contains .p8 file path

# File fastlane_core/lib/fastlane_core/itunes_transporter.rb, line 912
def api_key_with_p8_file_path(original_api_key)
  api_key = original_api_key.dup
  api_key[:key_dir] = Dir.mktmpdir("deliver-")
  # Specified p8 needs to be generated to call altool
  File.open(File.join(api_key[:key_dir], "AuthKey_#{api_key[:key_id]}.p8"), "wb") do |p8|
    p8.write(api_key[:key])
  end
  api_key
end
handle_error(password) click to toggle source
# File fastlane_core/lib/fastlane_core/itunes_transporter.rb, line 982
def handle_error(password)
  @transporter_executor.handle_error(password)
end
handle_two_step_failure(ex) click to toggle source

Tells the user how to get an application specific password

# File fastlane_core/lib/fastlane_core/itunes_transporter.rb, line 948
def handle_two_step_failure(ex)
  if ENV[TWO_FACTOR_ENV_VARIABLE].to_s.length > 0
    # Password provided, however we already used it
    UI.error("")
    UI.error("Application specific password you provided using")
    UI.error("environment variable #{TWO_FACTOR_ENV_VARIABLE}")
    UI.error("is invalid, please make sure it's correct")
    UI.error("")
    UI.user_error!("Invalid application specific password provided")
  end

  a = CredentialsManager::AccountManager.new(user: @user,
                                           prefix: TWO_STEP_HOST_PREFIX,
                                             note: "application-specific")
  if a.password(ask_if_missing: false).to_s.length > 0
    # user already entered one.. delete the old one
    UI.error("Application specific password seems wrong")
    UI.error("Please make sure to follow the instructions")
    a.remove_from_keychain
  end
  UI.error("")
  UI.error("Your account has 2 step verification enabled")
  UI.error("Please go to https://appleid.apple.com/account/manage")
  UI.error("and generate an application specific password for")
  UI.error("the iTunes Transporter, which is used to upload builds")
  UI.error("")
  UI.error("To set the application specific password on a CI machine using")
  UI.error("an environment variable, you can set the")
  UI.error("#{TWO_FACTOR_ENV_VARIABLE} variable")
  @password = a.password(ask_if_missing: true) # to ask the user for the missing value

  return true
end
load_password_for_transporter() click to toggle source

Returns the password to be used with the transporter

# File fastlane_core/lib/fastlane_core/itunes_transporter.rb, line 929
def load_password_for_transporter
  # 3 different sources for the password
  #   1) ENV variable for application specific password
  if ENV[TWO_FACTOR_ENV_VARIABLE].to_s.length > 0
    UI.message("Fetching password for transporter from environment variable named `#{TWO_FACTOR_ENV_VARIABLE}`")
    return ENV[TWO_FACTOR_ENV_VARIABLE]
  end
  #   2) TWO_STEP_HOST_PREFIX from keychain
  account_manager = CredentialsManager::AccountManager.new(user: @user,
                                                         prefix: TWO_STEP_HOST_PREFIX,
                                                           note: "application-specific")
  password = account_manager.password(ask_if_missing: false)
  return password if password.to_s.length > 0
  #   3) standard iTC password
  account_manager = CredentialsManager::AccountManager.new(user: @user)
  return account_manager.password(ask_if_missing: true)
end
should_use_altool?(altool_compatible_command, use_shell_script) click to toggle source

Returns whether altool should be used or ItunesTransporter should be used

# File fastlane_core/lib/fastlane_core/itunes_transporter.rb, line 923
def should_use_altool?(altool_compatible_command, use_shell_script)
  # Xcode 14 no longer supports iTMSTransporter. Use altool instead
  !use_shell_script && altool_compatible_command && !Helper.user_defined_itms_path? && Helper.mac? && Helper.xcode_at_least?(14)
end