class Fastlane::Actions::NotarizeAction

Public Class Methods

altool(params, package_path, bundle_id, try_early_stapling, print_log, verbose, api_key, compressed_package_path) click to toggle source
# File fastlane/lib/fastlane/actions/notarize.rb, line 105
def self.altool(params, package_path, bundle_id, try_early_stapling, print_log, verbose, api_key, compressed_package_path)
  UI.message('Uploading package to notarization service, might take a while')

  notarization_upload_command = "xcrun altool --notarize-app -t osx -f \"#{compressed_package_path || package_path}\" --primary-bundle-id #{bundle_id} --output-format xml"

  notarization_info = {}
  with_notarize_authenticator(params, api_key) do |notarize_authenticator|
    notarization_upload_command << " --asc-provider \"#{params[:asc_provider]}\"" if params[:asc_provider] && api_key.nil?

    notarization_upload_response = Actions.sh(
      notarize_authenticator.call(notarization_upload_command),
      log: verbose
    )

    FileUtils.rm_rf(compressed_package_path) if compressed_package_path

    notarization_upload_plist = Plist.parse_xml(notarization_upload_response)

    if notarization_upload_plist.key?('product-errors') && notarization_upload_plist['product-errors'].any?
      UI.important("🚫 Could not upload package to notarization service! Here are the reasons:")
      notarization_upload_plist['product-errors'].each { |product_error| UI.error("#{product_error['message']} (#{product_error['code']})") }
      UI.user_error!("Package upload to notarization service cancelled. Please check the error messages above.")
    end

    notarization_request_id = notarization_upload_plist['notarization-upload']['RequestUUID']

    UI.success("Successfully uploaded package to notarization service with request identifier #{notarization_request_id}")

    while notarization_info.empty? || (notarization_info['Status'] == 'in progress')
      if notarization_info.empty?
        UI.message('Waiting to query request status')
      elsif try_early_stapling
        UI.message('Request in progress, trying early staple')

        begin
          self.staple(package_path, verbose)
          UI.message('Successfully notarized and early stapled package.')

          return
        rescue
          UI.message('Early staple failed, waiting to query again')
        end
      end

      sleep(30)

      UI.message('Querying request status')

      # As of July 2020, the request UUID might not be available for polling yet which returns an error code
      # This is now handled with the error_callback (which prevents an error from being raised)
      # Catching this error allows for polling to continue until the notarization is complete
      error = false
      notarization_info_response = Actions.sh(
        notarize_authenticator.call("xcrun altool --notarization-info #{notarization_request_id} --output-format xml"),
        log: verbose,
        error_callback: lambda { |msg|
          error = true
          UI.error("Error polling for notarization info: #{msg}")
        }
      )

      unless error
        notarization_info_plist = Plist.parse_xml(notarization_info_response)
        notarization_info = notarization_info_plist['notarization-info']
      end
    end
  end
  # rubocop:enable Metrics/PerceivedComplexity

  log_url = notarization_info['LogFileURL']
  ENV['FL_NOTARIZE_LOG_FILE_URL'] = log_url
  log_suffix = ''
  if log_url && print_log
    log_response = Net::HTTP.get(URI(log_url))
    log_json_object = JSON.parse(log_response)
    log_suffix = ", with log:\n#{JSON.pretty_generate(log_json_object)}"
  end

  case notarization_info['Status']
  when 'success'
    UI.message('Stapling package')

    self.staple(package_path, verbose)

    UI.success("Successfully notarized and stapled package#{log_suffix}")
  when 'invalid'
    UI.user_error!("Could not notarize package with message '#{notarization_info['Status Message']}'#{log_suffix}")
  else
    UI.crash!("Could not notarize package with status '#{notarization_info['Status']}'#{log_suffix}")
  end
ensure
  ENV.delete('FL_NOTARIZE_PASSWORD')
end
authors() click to toggle source
# File fastlane/lib/fastlane/actions/notarize.rb, line 238
def self.authors
  ['zeplin']
end
available_options() click to toggle source
# File fastlane/lib/fastlane/actions/notarize.rb, line 242
def self.available_options
  username = CredentialsManager::AppfileConfig.try_fetch_value(:apple_dev_portal_id)
  username ||= CredentialsManager::AppfileConfig.try_fetch_value(:apple_id)

  asc_provider = CredentialsManager::AppfileConfig.try_fetch_value(:itc_team_id)

  [
    FastlaneCore::ConfigItem.new(key: :package,
                                 env_name: 'FL_NOTARIZE_PACKAGE',
                                 description: 'Path to package to notarize, e.g. .app bundle or disk image',
                                 verify_block: proc do |value|
                                   UI.user_error!("Could not find package at '#{value}'") unless File.exist?(value)
                                 end),
    FastlaneCore::ConfigItem.new(key: :use_notarytool,
                                 env_name: 'FL_NOTARIZE_USE_NOTARYTOOL',
                                 description: 'Whether to `xcrun notarytool` or `xcrun altool`',
                                 default_value: Helper.mac? && Helper.xcode_at_least?("13.0"), # Notary tool added in Xcode 13
                                 default_value_dynamic: true,
                                 type: Boolean),
    FastlaneCore::ConfigItem.new(key: :try_early_stapling,
                                 env_name: 'FL_NOTARIZE_TRY_EARLY_STAPLING',
                                 description: 'Whether to try early stapling while the notarization request is in progress',
                                 optional: true,
                                 default_value: false,
                                 type: Boolean),
    FastlaneCore::ConfigItem.new(key: :bundle_id,
                                 env_name: 'FL_NOTARIZE_BUNDLE_ID',
                                 description: 'Bundle identifier to uniquely identify the package',
                                 optional: true),
    FastlaneCore::ConfigItem.new(key: :username,
                                 env_name: 'FL_NOTARIZE_USERNAME',
                                 description: 'Apple ID username',
                                 default_value: username,
                                 optional: true,
                                 conflicting_options: [:api_key_path, :api_key],
                                 default_value_dynamic: true),
    FastlaneCore::ConfigItem.new(key: :asc_provider,
                                 env_name: 'FL_NOTARIZE_ASC_PROVIDER',
                                 description: 'Provider short name for accounts associated with multiple providers',
                                 optional: true,
                                 default_value: asc_provider),
    FastlaneCore::ConfigItem.new(key: :print_log,
                                 env_name: 'FL_NOTARIZE_PRINT_LOG',
                                 description: 'Whether to print notarization log file, listing issues on failure and warnings on success',
                                 optional: true,
                                 default_value: false,
                                 type: Boolean),
    FastlaneCore::ConfigItem.new(key: :verbose,
                                 env_name: 'FL_NOTARIZE_VERBOSE',
                                 description: 'Whether to log requests',
                                 optional: true,
                                 default_value: false,
                                 type: Boolean),
    FastlaneCore::ConfigItem.new(key: :api_key_path,
                                 env_names: ['FL_NOTARIZE_API_KEY_PATH', "APP_STORE_CONNECT_API_KEY_PATH"],
                                 description: "Path to your App Store Connect API Key JSON file (https://docs.fastlane.tools/app-store-connect-api/#using-fastlane-api-key-json-file)",
                                 optional: true,
                                 conflicting_options: [:username, :api_key],
                                 verify_block: proc do |value|
                                   UI.user_error!("API Key not found at '#{value}'") unless File.exist?(value)
                                 end),
    FastlaneCore::ConfigItem.new(key: :api_key,
                                 env_names: ['FL_NOTARIZE_API_KEY', "APP_STORE_CONNECT_API_KEY"],
                                 description: "Your App Store Connect API Key information (https://docs.fastlane.tools/app-store-connect-api/#using-fastlane-api-key-hash-option)",
                                 optional: true,
                                 conflicting_options: [:username, :api_key_path],
                                 sensitive: true,
                                 type: Hash)
  ]
end
category() click to toggle source
# File fastlane/lib/fastlane/actions/notarize.rb, line 317
def self.category
  :code_signing
end
description() click to toggle source
# File fastlane/lib/fastlane/actions/notarize.rb, line 234
def self.description
  'Notarizes a macOS app'
end
is_supported?(platform) click to toggle source
# File fastlane/lib/fastlane/actions/notarize.rb, line 313
def self.is_supported?(platform)
  platform == :mac
end
notarytool(params, package_path, bundle_id, try_early_stapling, print_log, verbose, api_key, compressed_package_path) click to toggle source
# File fastlane/lib/fastlane/actions/notarize.rb, line 47
def self.notarytool(params, package_path, bundle_id, try_early_stapling, print_log, verbose, api_key, compressed_package_path)
  temp_file = nil

  # Create authorization part of command with either API Key or Apple ID
  auth_parts = []
  if api_key
    # Writes key contents to temporary file for command
    require 'tempfile'
    temp_file = Tempfile.new
    api_key.write_key_to_file(temp_file.path)

    auth_parts << "--key #{temp_file.path}"
    auth_parts << "--key-id #{api_key.key_id}"
    auth_parts << "--issuer #{api_key.issuer_id}"
  else
    auth_parts << "--apple-id #{params[:username]}"
    auth_parts << "--password #{ENV['FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD']}"
    auth_parts << "--team-id #{params[:asc_provider]}"
  end

  # Submits package and waits for processing using `xcrun notarytool submit --wait`
  submit_parts = [
    "xcrun notarytool submit",
    (compressed_package_path || package_path).shellescape,
    "--output-format json",
    "--wait"
  ] + auth_parts

  submit_command = submit_parts.join(' ')
  submit_response = Actions.sh(
    submit_command,
    log: verbose,
    error_callback: lambda { |msg|
      UI.error("Error polling for notarization info: #{msg}")
    }
  )

  notarization_info = JSON.parse(submit_response)

  # Staple
  case notarization_info['status']
  when 'Accepted'
    submission_id = notarization_info["id"]
    UI.success("Successfully uploaded package to notarization service with request identifier #{submission_id}")

    UI.message('Stapling package')
    self.staple(package_path, verbose)

    UI.success("Successfully notarized and stapled package")
  when 'Invalid'
    UI.user_error!("Could not notarize package with message '#{notarization_info['statusSummary']}'")
  else
    UI.crash!("Could not notarize package with status '#{notarization_info['status']}'")
  end
ensure
  temp_file.delete if temp_file
end
run(params) click to toggle source

rubocop:disable Metrics/PerceivedComplexity

# File fastlane/lib/fastlane/actions/notarize.rb, line 5
def self.run(params)
  package_path = params[:package]
  bundle_id = params[:bundle_id]
  try_early_stapling = params[:try_early_stapling]
  print_log = params[:print_log]
  verbose = params[:verbose]

  # Only set :api_key from SharedValues if :api_key_path isn't set (conflicting options)
  unless params[:api_key_path]
    params[:api_key] ||= Actions.lane_context[SharedValues::APP_STORE_CONNECT_API_KEY]
  end
  api_key = Spaceship::ConnectAPI::Token.from(hash: params[:api_key], filepath: params[:api_key_path])

  use_notarytool = params[:use_notarytool]

  # Compress and read bundle identifier only for .app bundle.
  compressed_package_path = nil
  if File.extname(package_path) == '.app'
    compressed_package_path = "#{package_path}.zip"
    Actions.sh(
      "ditto -c -k --rsrc --keepParent \"#{package_path}\" \"#{compressed_package_path}\"",
      log: verbose
    )

    unless bundle_id
      info_plist_path = File.join(package_path, 'Contents', 'Info.plist')
      bundle_id = Actions.sh(
        "/usr/libexec/PlistBuddy -c \"Print :CFBundleIdentifier\" \"#{info_plist_path}\"",
        log: verbose
      ).strip
    end
  end

  UI.user_error!('Could not read bundle identifier, provide as a parameter') unless bundle_id

  if use_notarytool
    notarytool(params, package_path, bundle_id, try_early_stapling, print_log, verbose, api_key, compressed_package_path)
  else
    altool(params, package_path, bundle_id, try_early_stapling, print_log, verbose, api_key, compressed_package_path)
  end
end
staple(package_path, verbose) click to toggle source
# File fastlane/lib/fastlane/actions/notarize.rb, line 199
def self.staple(package_path, verbose)
  Actions.sh(
    "xcrun stapler staple #{package_path.shellescape}",
    log: verbose
  )
end
with_notarize_authenticator(params, api_key) { |proc| ... } click to toggle source
# File fastlane/lib/fastlane/actions/notarize.rb, line 206
def self.with_notarize_authenticator(params, api_key)
  if api_key
    # From xcrun altool for --apiKey:
    # This option will search the following directories in sequence for a private key file with the name of 'AuthKey_<api_key>.p8':  './private_keys', '~/private_keys', '~/.private_keys', and '~/.appstoreconnect/private_keys'.
    api_key_folder_path = File.expand_path('~/.appstoreconnect/private_keys')
    api_key_file_path = File.join(api_key_folder_path, "AuthKey_#{api_key.key_id}.p8")
    directory_exists = File.directory?(api_key_folder_path)
    file_exists = File.exist?(api_key_file_path)
    begin
      FileUtils.mkdir_p(api_key_folder_path) unless directory_exists
      api_key.write_key_to_file(api_key_file_path) unless file_exists

      yield(proc { |command| "#{command} --apiKey #{api_key.key_id} --apiIssuer #{api_key.issuer_id}" })
    ensure
      FileUtils.rm(api_key_file_path) unless file_exists
      FileUtils.rm_r(api_key_folder_path) unless directory_exists
    end
  else
    apple_id_account = CredentialsManager::AccountManager.new(user: params[:username])

    # Add password as a temporary environment variable for altool.
    # Use app specific password if specified.
    ENV['FL_NOTARIZE_PASSWORD'] = ENV['FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD'] || apple_id_account.password

    yield(proc { |command| "#{command} -u #{apple_id_account.user} -p @env:FL_NOTARIZE_PASSWORD" })
  end
end