class Fastlane::Actions::DownloadDsymsAction

Public Class Methods

authors() click to toggle source
# File fastlane/lib/fastlane/actions/download_dsyms.rb, line 350
def self.authors
  ["KrauseFx"]
end
available_options() click to toggle source
# File fastlane/lib/fastlane/actions/download_dsyms.rb, line 232
def self.available_options
  user = CredentialsManager::AppfileConfig.try_fetch_value(:itunes_connect_id)
  user ||= CredentialsManager::AppfileConfig.try_fetch_value(:apple_id)

  [
    FastlaneCore::ConfigItem.new(key: :api_key_path,
                                 env_names: ["DOWNLOAD_DSYMS_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: [:api_key],
                                 verify_block: proc do |value|
                                   UI.user_error!("Couldn't find API key JSON file at path '#{value}'") unless File.exist?(value)
                                 end),
    FastlaneCore::ConfigItem.new(key: :api_key,
                                 env_names: ["DOWNLOAD_DSYMS_API_KEY", "APP_STORE_CONNECT_API_KEY"],
                                 description: "Your App Store Connect API Key information (https://docs.fastlane.tools/app-store-connect-api/#use-return-value-and-pass-in-as-an-option)",
                                 type: Hash,
                                 default_value: Fastlane::Actions.lane_context[Fastlane::Actions::SharedValues::APP_STORE_CONNECT_API_KEY],
                                 default_value_dynamic: true,
                                 optional: true,
                                 sensitive: true,
                                 conflicting_options: [:api_key_path]),
    FastlaneCore::ConfigItem.new(key: :username,
                                 short_option: "-u",
                                 env_name: "DOWNLOAD_DSYMS_USERNAME",
                                 description: "Your Apple ID Username for App Store Connect",
                                 default_value: user,
                                 default_value_dynamic: true),
    FastlaneCore::ConfigItem.new(key: :app_identifier,
                                 short_option: "-a",
                                 env_name: "DOWNLOAD_DSYMS_APP_IDENTIFIER",
                                 description: "The bundle identifier of your app",
                                 optional: false,
                                 code_gen_sensitive: true,
                                 default_value: CredentialsManager::AppfileConfig.try_fetch_value(:app_identifier),
                                 default_value_dynamic: true),
    FastlaneCore::ConfigItem.new(key: :team_id,
                                 short_option: "-k",
                                 env_name: "DOWNLOAD_DSYMS_TEAM_ID",
                                 description: "The ID of your App Store Connect team if you're in multiple teams",
                                 optional: true,
                                 skip_type_validation: true, # as we also allow integers, which we convert to strings anyway
                                 code_gen_sensitive: true,
                                 default_value: CredentialsManager::AppfileConfig.try_fetch_value(:itc_team_id),
                                 default_value_dynamic: true,
                                 verify_block: proc do |value|
                                   ENV["FASTLANE_ITC_TEAM_ID"] = value.to_s
                                 end),
    FastlaneCore::ConfigItem.new(key: :team_name,
                                 short_option: "-e",
                                 env_name: "DOWNLOAD_DSYMS_TEAM_NAME",
                                 description: "The name of your App Store Connect team if you're in multiple teams",
                                 optional: true,
                                 code_gen_sensitive: true,
                                 default_value: CredentialsManager::AppfileConfig.try_fetch_value(:itc_team_name),
                                 default_value_dynamic: true,
                                 verify_block: proc do |value|
                                   ENV["FASTLANE_ITC_TEAM_NAME"] = value.to_s
                                 end),
    FastlaneCore::ConfigItem.new(key: :platform,
                                 short_option: "-p",
                                 env_name: "DOWNLOAD_DSYMS_PLATFORM",
                                 description: "The app platform for dSYMs you wish to download (ios, appletvos)",
                                 default_value: :ios),
    FastlaneCore::ConfigItem.new(key: :version,
                                 short_option: "-v",
                                 env_name: "DOWNLOAD_DSYMS_VERSION",
                                 description: "The app version for dSYMs you wish to download, pass in 'latest' to download only the latest build's dSYMs or 'live' to download only the live version dSYMs",
                                 optional: true),
    FastlaneCore::ConfigItem.new(key: :build_number,
                                 short_option: "-b",
                                 env_name: "DOWNLOAD_DSYMS_BUILD_NUMBER",
                                 description: "The app build_number for dSYMs you wish to download",
                                 optional: true,
                                 skip_type_validation: true), # as we also allow integers, which we convert to strings anyway
    FastlaneCore::ConfigItem.new(key: :min_version,
                                 short_option: "-m",
                                 env_name: "DOWNLOAD_DSYMS_MIN_VERSION",
                                 description: "The minimum app version for dSYMs you wish to download",
                                 optional: true),
    FastlaneCore::ConfigItem.new(key: :after_uploaded_date,
                                 short_option: "-d",
                                 env_name: "DOWNLOAD_DSYMS_AFTER_UPLOADED_DATE",
                                 description: "The uploaded date after which you wish to download dSYMs",
                                 optional: true),
    FastlaneCore::ConfigItem.new(key: :output_directory,
                                 short_option: "-s",
                                 env_name: "DOWNLOAD_DSYMS_OUTPUT_DIRECTORY",
                                 description: "Where to save the download dSYMs, defaults to the current path",
                                 optional: true),
    FastlaneCore::ConfigItem.new(key: :wait_for_dsym_processing,
                                 short_option: "-w",
                                 env_name: "DOWNLOAD_DSYMS_WAIT_FOR_DSYM_PROCESSING",
                                 description: "Wait for dSYMs to process",
                                 optional: true,
                                 default_value: false,
                                 type: Boolean),
    FastlaneCore::ConfigItem.new(key: :wait_timeout,
                                 short_option: "-t",
                                 env_name: "DOWNLOAD_DSYMS_WAIT_TIMEOUT",
                                 description: "Number of seconds to wait for dSYMs to process",
                                 optional: true,
                                 default_value: 300,
                                 type: Integer)
  ]
end
category() click to toggle source
# File fastlane/lib/fastlane/actions/download_dsyms.rb, line 369
def self.category
  :app_store_connect
end
description() click to toggle source

@!group Documentation

# File fastlane/lib/fastlane/actions/download_dsyms.rb, line 211
def self.description
  "Download dSYM files from App Store Connect for Bitcode apps"
end
details() click to toggle source
# File fastlane/lib/fastlane/actions/download_dsyms.rb, line 215
      def self.details
        sample = <<-SAMPLE.markdown_sample
          ```ruby
          lane :refresh_dsyms do
            download_dsyms                  # Download dSYM files from iTC
            upload_symbols_to_crashlytics   # Upload them to Crashlytics
            clean_build_artifacts           # Delete the local dSYM files
          end
          ```
        SAMPLE

        [
          "This action downloads dSYM files from App Store Connect after the ipa gets re-compiled by Apple. Useful if you have Bitcode enabled.".markdown_preserve_newlines,
          sample
        ].join("\n")
      end
download(download_url, build, app, output_directory) click to toggle source
# File fastlane/lib/fastlane/actions/download_dsyms.rb, line 167
def self.download(download_url, build, app, output_directory)
  result = self.download_file(download_url)
  path   = write_dsym(result, app.bundle_id, build.app_version, build.version, output_directory)
  UI.success("🔑  Successfully downloaded dSYM file for #{build.app_version} - #{build.version} to '#{path}'")

  Actions.lane_context[SharedValues::DSYM_PATHS] ||= []
  Actions.lane_context[SharedValues::DSYM_PATHS] << File.expand_path(path)

  unless build.uploaded_date.nil?
    Actions.lane_context[SharedValues::DSYM_LATEST_UPLOADED_DATE] ||= build.uploaded_date
    current_latest = Actions.lane_context[SharedValues::DSYM_LATEST_UPLOADED_DATE]
    Actions.lane_context[SharedValues::DSYM_LATEST_UPLOADED_DATE] = [current_latest, build.uploaded_date].max
    UI.verbose("Most recent build uploaded_date #{Actions.lane_context[SharedValues::DSYM_LATEST_UPLOADED_DATE]}")
  end
end
download_dsym(build_bundle: nil, build: nil, app: nil, wait_for_dsym_processing: nil, wait_timeout: nil, output_directory: nil) click to toggle source
# File fastlane/lib/fastlane/actions/download_dsyms.rb, line 124
def self.download_dsym(build_bundle: nil, build: nil, app: nil, wait_for_dsym_processing: nil, wait_timeout: nil, output_directory: nil)
  start = Time.now
  download_url = nil

  loop do
    download_url = build_bundle.dsym_url

    unless download_url
      if !wait_for_dsym_processing || (Time.now - start) > wait_timeout
        # In some cases, AppStoreConnect does not process the dSYMs, thus no error should be thrown.
        UI.message("Could not find any dSYM for #{build.version} (#{build.app_version})")
      else
        UI.message("Waiting for dSYM file to appear...")
        sleep(30)
        next
      end
    end

    break
  end

  if download_url
    self.download(download_url, build, app, output_directory)
    return if build.version
  else
    UI.message("No dSYM URL for #{build.version} (#{build.app_version})")
  end
end
download_file(url) click to toggle source
# File fastlane/lib/fastlane/actions/download_dsyms.rb, line 192
def self.download_file(url)
  uri = URI.parse(url)
  if ENV['http_proxy']
    UI.verbose("Found 'http_proxy' environment variable so connect via proxy")
    proxy_uri = URI.parse(ENV['http_proxy'])
    http = Net::HTTP.new(uri.host, uri.port, proxy_uri.host, proxy_uri.port)
  else
    http = Net::HTTP.new(uri.host, uri.port)
  end
  http.read_timeout = 300
  http.use_ssl = (uri.scheme == "https")
  res = http.get(uri.request_uri)
  res.body
end
example_code() click to toggle source
# File fastlane/lib/fastlane/actions/download_dsyms.rb, line 358
def self.example_code
  [
    'download_dsyms',
    'download_dsyms(version: "1.0.0", build_number: "345")',
    'download_dsyms(version: "1.0.1", build_number: 42)',
    'download_dsyms(version: "live")',
    'download_dsyms(min_version: "1.2.3")',
    'download_dsyms(after_uploaded_date: "2020-09-11T19:00:00+01:00")'
  ]
end
get_latest_build!(app_id: nil, platform: nil) click to toggle source

rubocop:enable Metrics/PerceivedComplexity

# File fastlane/lib/fastlane/actions/download_dsyms.rb, line 154
def self.get_latest_build!(app_id: nil, platform: nil)
  filter = { app: app_id }
  filter["preReleaseVersion.platform"] = platform
  latest_build = Spaceship::ConnectAPI.get_builds(filter: filter, sort: "-uploadedDate", includes: "preReleaseVersion,buildBundles").first

  if latest_build.nil?
    UI.user_error!("Could not find any build for platform #{platform}") if platform
    UI.user_error!("Could not find any build")
  end

  return latest_build
end
is_supported?(platform) click to toggle source
# File fastlane/lib/fastlane/actions/download_dsyms.rb, line 354
def self.is_supported?(platform)
  [:ios, :appletvos].include?(platform)
end
output() click to toggle source
# File fastlane/lib/fastlane/actions/download_dsyms.rb, line 339
def self.output
  [
    ['DSYM_PATHS', 'An array to all the zipped dSYM files'],
    ['DSYM_LATEST_UPLOADED_DATE', 'Date of the most recent uploaded time of successfully downloaded dSYM files']
  ]
end
return_value() click to toggle source
# File fastlane/lib/fastlane/actions/download_dsyms.rb, line 346
def self.return_value
  nil
end
run(params) click to toggle source

rubocop:disable Metrics/PerceivedComplexity

# File fastlane/lib/fastlane/actions/download_dsyms.rb, line 9
def self.run(params)
  require 'openssl'
  require 'spaceship'
  require 'net/http'
  require 'date'

  if (api_token = Spaceship::ConnectAPI::Token.from(hash: params[:api_key], filepath: params[:api_key_path]))
    UI.message("Creating authorization token for App Store Connect API")
    Spaceship::ConnectAPI.token = api_token
  elsif !Spaceship::ConnectAPI.token.nil?
    UI.message("Using existing authorization token for App Store Connect API")
  else
    # Team selection passed though FASTLANE_ITC_TEAM_ID and FASTLANE_ITC_TEAM_NAME environment variables
    # Prompts select team if multiple teams and none specified
    UI.message("Login to App Store Connect (#{params[:username]})")
    Spaceship::ConnectAPI.login(params[:username], use_portal: false, use_tunes: true)
    UI.message("Login successful")
  end

  # Get App
  app = Spaceship::ConnectAPI::App.find(params[:app_identifier])
  unless app
    UI.user_error!("Could not find app with bundle identifier '#{params[:app_identifier]}' on account #{params[:username]}")
  end

  # Process options
  version = params[:version]
  build_number = params[:build_number].to_s unless params[:build_number].nil?
  itc_platform = params[:platform]
  output_directory = params[:output_directory]
  wait_for_dsym_processing = params[:wait_for_dsym_processing]
  wait_timeout = params[:wait_timeout]
  min_version = Gem::Version.new(params[:min_version]) if params[:min_version]
  after_uploaded_date = DateTime.parse(params[:after_uploaded_date]) unless params[:after_uploaded_date].nil?

  platform = Spaceship::ConnectAPI::Platform.map(itc_platform)

  # Set version if it is latest
  if version == 'latest'
    # Try to grab the edit version first, else fallback to live version
    UI.message("Looking for latest build...")
    latest_build = get_latest_build!(app_id: app.id, platform: platform)
    version = latest_build.app_version
    build_number = latest_build.version
  elsif version == 'live'
    UI.message("Looking for live version...")
    live_version = app.get_live_app_store_version(platform: platform)

    UI.user_error!("Could not find live version for your app, please try setting 'latest' or a specific version") if live_version.nil?

    # No need to search for candidates, because released App Store version should only have one build
    version = live_version.version_string
    build_number = live_version.build.version
  end

  # Make sure output_directory has a slash on the end
  if output_directory && !output_directory.end_with?('/')
    output_directory += '/'
  end

  # Write a nice message
  message = []
  message << "Looking for dSYM files for '#{params[:app_identifier]}' on platform #{platform}"
  message << "v#{version}" if version
  message << "(#{build_number})" if build_number
  UI.message(message.join(" "))

  filter = { app: app.id }
  filter["preReleaseVersion.platform"] = platform
  build_resps = Spaceship::ConnectAPI.get_builds(filter: filter, sort: "-uploadedDate", includes: "preReleaseVersion,buildBundles").all_pages
  builds = build_resps.flat_map(&:to_models)

  builds.each do |build|
    asc_app_version = build.app_version
    asc_build_number = build.version
    uploaded_date = DateTime.parse(build.uploaded_date)

    message = []
    message << "Found train (version): #{asc_app_version}"
    message << ", comparing to supplied version: #{version}" if version
    UI.verbose(message.join(" "))

    if version && version != asc_app_version
      UI.verbose("Version #{version} doesn't match: #{asc_app_version}")
      next
    end

    if min_version && min_version > Gem::Version.new(asc_app_version)
      UI.verbose("Min version #{min_version} not reached: #{asc_app_version}")
      next
    end

    if after_uploaded_date && after_uploaded_date >= uploaded_date
      UI.verbose("Upload date #{after_uploaded_date} not reached: #{uploaded_date}")
      next
    end

    message = []
    message << "Found build version: #{asc_build_number}"
    message << ", comparing to supplied build_number: #{build_number}" if build_number
    UI.verbose(message.join(" "))

    if build_number && asc_build_number != build_number
      UI.verbose("build_version: #{asc_build_number} doesn't match: #{build_number}")
      next
    end

    UI.verbose("Build_version: #{asc_build_number} matches #{build_number}, grabbing dsym_url") if build_number

    build.build_bundles&.each do |build_bundle|
      download_dsym(build_bundle: build_bundle, build: build, app: app, wait_for_dsym_processing: wait_for_dsym_processing, wait_timeout: wait_timeout, output_directory: output_directory)
    end
  end
end
write_dsym(data, bundle_id, train_number, build_number, output_directory) click to toggle source
# File fastlane/lib/fastlane/actions/download_dsyms.rb, line 183
def self.write_dsym(data, bundle_id, train_number, build_number, output_directory)
  file_name = "#{bundle_id}-#{train_number}-#{build_number}.dSYM.zip"
  if output_directory
    file_name = output_directory + file_name
  end
  File.binwrite(file_name, data)
  file_name
end