class Fastlane::Actions::AppcenterUploadAction

Public Class Methods

add_app_to_distribution_group_if_needed(params) click to toggle source
# File lib/fastlane/plugin/appcenter/actions/appcenter_upload_action.rb, line 321
def self.add_app_to_distribution_group_if_needed(params)
  return unless params[:destination_type] == 'group' && params[:owner_type] == 'organization' && params[:destinations] != '*'

  app_distribution_groups = Helper::AppcenterHelper.fetch_distribution_groups(
    api_token: params[:api_token],
    owner_name: params[:owner_name],
    app_name: params[:app_name]
  )

  group_names = app_distribution_groups.map { |g| g['name'] }
  destination_names = params[:destinations].split(',').map(&:strip)

  destination_names.each do |destination_name|
    unless group_names.include? destination_name
      Helper::AppcenterHelper.add_new_app_to_distribution_group(
        api_token: params[:api_token],
        owner_name: params[:owner_name],
        app_name: params[:app_name],
        destination_name: destination_name
      )
    end
  end
end
authors() click to toggle source
# File lib/fastlane/plugin/appcenter/actions/appcenter_upload_action.rb, line 370
def self.authors
  ["Microsoft"]
end
available_options() click to toggle source
# File lib/fastlane/plugin/appcenter/actions/appcenter_upload_action.rb, line 378
def self.available_options
  [
    FastlaneCore::ConfigItem.new(key: :api_token,
                            env_name: "APPCENTER_API_TOKEN",
                         description: "API Token for App Center",
                       default_value: Actions.lane_context[SharedValues::APPCENTER_API_TOKEN],
                            optional: false,
                                type: String,
                        verify_block: proc do |value|
                          UI.user_error!("No API token for App Center given, pass using `api_token: 'token'`") unless value && !value.empty?
                        end),

    FastlaneCore::ConfigItem.new(key: :owner_type,
                            env_name: "APPCENTER_OWNER_TYPE",
                         description: "Owner type, either 'user' or 'organization'",
                            optional: true,
                       default_value: "user",
                                type: String,
                        verify_block: proc do |value|
                          accepted_formats = ["user", "organization"]
                          UI.user_error!("Only \"user\" and \"organization\" types are allowed, you provided \"#{value}\"") unless accepted_formats.include? value
                        end),

    FastlaneCore::ConfigItem.new(key: :owner_name,
                            env_name: "APPCENTER_OWNER_NAME",
                         description: "Owner name as found in the App's URL in App Center",
                       default_value: Actions.lane_context[SharedValues::APPCENTER_OWNER_NAME],
                            optional: false,
                                type: String,
                        verify_block: proc do |value|
                          UI.user_error!("No Owner name for App Center given, pass using `owner_name: 'name'`") unless value && !value.empty?
                        end),

    FastlaneCore::ConfigItem.new(key: :app_name,
                            env_name: "APPCENTER_APP_NAME",
                         description: "App name as found in the App's URL in App Center. If there is no app with such name, you will be prompted to create one",
                       default_value: Actions.lane_context[SharedValues::APPCENTER_APP_NAME],
                            optional: false,
                                type: String,
                        verify_block: proc do |value|
                          UI.user_error!("No App name given, pass using `app_name: 'app name'`") unless value && !value.empty?
                        end),

    FastlaneCore::ConfigItem.new(key: :app_display_name,
                            env_name: "APPCENTER_APP_DISPLAY_NAME",
                         description: "App display name to use when creating a new app",
                            optional: true,
                                type: String),

    FastlaneCore::ConfigItem.new(key: :app_os,
                            env_name: "APPCENTER_APP_OS",
                         description: "App OS can be Android, iOS, macOS, Windows, Custom. Used for new app creation, if app 'app_name' was not found",
                            optional: true,
                                type: String),

    FastlaneCore::ConfigItem.new(key: :app_platform,
                            env_name: "APPCENTER_APP_PLATFORM",
                         description: "App Platform. Used for new app creation, if app 'app_name' was not found",
                            optional: true,
                                type: String),

    FastlaneCore::ConfigItem.new(key: :apk,
                            env_name: "APPCENTER_DISTRIBUTE_APK",
                         description: "Build release path for android build",
                       default_value: Actions.lane_context[SharedValues::GRADLE_APK_OUTPUT_PATH],
                            optional: true,
                          deprecated: true,
                                type: String,
                 conflicting_options: [:ipa, :aab, :file],
                      conflict_block: proc do |value|
                        UI.user_error!("You can't use 'apk' and '#{value.key}' options in one run")
                      end,
                        verify_block: proc do |value|
                          accepted_formats = [".apk"]
                          file_extname_full = Helper::AppcenterHelper.file_extname_full(value)
                          self.optional_error("Only \".apk\" formats are allowed, you provided \"#{file_extname_full}\"") unless accepted_formats.include? file_extname_full
                        end),

    FastlaneCore::ConfigItem.new(key: :aab,
                            env_name: "APPCENTER_DISTRIBUTE_AAB",
                         description: "Build release path for android app bundle build",
                       default_value: Actions.lane_context[SharedValues::GRADLE_AAB_OUTPUT_PATH],
                            optional: true,
                          deprecated: true,
                                type: String,
                 conflicting_options: [:ipa, :apk, :file],
                      conflict_block: proc do |value|
                        UI.user_error!("You can't use 'aab' and '#{value.key}' options in one run")
                      end,
                        verify_block: proc do |value|
                          accepted_formats = [".aab"]
                          self.optional_error("Only \".aab\" formats are allowed, you provided \"#{File.extname(value)}\"") unless accepted_formats.include? File.extname(value)
                        end),

    FastlaneCore::ConfigItem.new(key: :ipa,
                            env_name: "APPCENTER_DISTRIBUTE_IPA",
                         description: "Build release path for iOS builds",
                       default_value: Actions.lane_context[SharedValues::IPA_OUTPUT_PATH],
                            optional: true,
                          deprecated: true,
                                type: String,
                 conflicting_options: [:apk, :aab, :file],
                      conflict_block: proc do |value|
                        UI.user_error!("You can't use 'ipa' and '#{value.key}' options in one run")
                      end,
                        verify_block: proc do |value|
                          accepted_formats = [".ipa"]
                          self.optional_error("Only \".ipa\" formats are allowed, you provided \"#{File.extname(value)}\"") unless accepted_formats.include? File.extname(value)
                        end),

    FastlaneCore::ConfigItem.new(key: :file,
                            env_name: "APPCENTER_DISTRIBUTE_FILE",
                         description: "File path to the release build to publish",
                            optional: true,
                                type: String,
                 conflicting_options: [:apk, :aab, :ipa],
                      conflict_block: proc do |value|
                        UI.user_error!("You can't use 'file' and '#{value.key}' options in one run")
                      end,
                        verify_block: proc do |value|
                          platform = Actions.lane_context[SharedValues::PLATFORM_NAME]
                          if platform
                            accepted_formats = Constants::SUPPORTED_EXTENSIONS[platform.to_sym]
                            unless accepted_formats
                              UI.important("Unknown platform '#{platform}', Supported are #{Constants::SUPPORTED_EXTENSIONS.keys}")
                              accepted_formats = Constants::ALL_SUPPORTED_EXTENSIONS
                            end
                            file_ext = Helper::AppcenterHelper.file_extname_full(value)
                            self.optional_error("Extension not supported: '#{file_ext}'. Supported formats for platform '#{platform}': #{accepted_formats.join ' '}") unless accepted_formats.include? file_ext
                          end
                        end),

    FastlaneCore::ConfigItem.new(key: :upload_build_only,
                            env_name: "APPCENTER_DISTRIBUTE_UPLOAD_BUILD_ONLY",
                         description: "Flag to upload only the build to App Center. Skips uploading symbols or mapping",
                            optional: true,
                           is_string: false,
                       default_value: false,
                 conflicting_options: [:upload_dsym_only, :upload_mapping_only],
                      conflict_block: proc do |value|
                        UI.user_error!("You can't use 'upload_build_only' and '#{value.key}' options in one run")
                      end),

    FastlaneCore::ConfigItem.new(key: :dsym,
                            env_name: "APPCENTER_DISTRIBUTE_DSYM",
                         description: "Path to your symbols file. For iOS provide path to app.dSYM.zip",
                       default_value: Actions.lane_context[SharedValues::DSYM_OUTPUT_PATH],
                            optional: true,
                                type: String,
                        verify_block: proc do |value|
                          deprecated_files = [".txt"]
                          if value
                            UI.user_error!("Couldn't find dSYM file at path '#{value}'") unless File.exist?(value)
                            UI.message("Support for *.txt has been deprecated. Please use --mapping parameter or APPCENTER_DISTRIBUTE_ANDROID_MAPPING environment variable instead.") if deprecated_files.include? File.extname(value)
                          end
                        end),

    FastlaneCore::ConfigItem.new(key: :upload_dsym_only,
                            env_name: "APPCENTER_DISTRIBUTE_UPLOAD_DSYM_ONLY",
                         description: "Flag to upload only the dSYM file to App Center",
                            optional: true,
                           is_string: false,
                       default_value: false),

    FastlaneCore::ConfigItem.new(key: :mapping,
                            env_name: "APPCENTER_DISTRIBUTE_ANDROID_MAPPING",
                         description: "Path to your Android mapping.txt",
                         default_value: (defined? SharedValues::GRADLE_MAPPING_TXT_OUTPUT_PATH) && Actions.lane_context[SharedValues::GRADLE_MAPPING_TXT_OUTPUT_PATH] || nil,
                            optional: true,
                                type: String,
                        verify_block: proc do |value|
                          accepted_formats = [".txt"]
                          if value
                            UI.user_error!("Couldn't find mapping file at path '#{value}'") unless File.exist?(value)
                            UI.user_error!("Only \"*.txt\" formats are allowed, you provided \"#{File.name(value)}\"") unless accepted_formats.include? File.extname(value)
                          end
                        end),

    FastlaneCore::ConfigItem.new(key: :upload_mapping_only,
                            env_name: "APPCENTER_DISTRIBUTE_UPLOAD_ANDROID_MAPPING_ONLY",
                         description: "Flag to upload only the mapping.txt file to App Center",
                            optional: true,
                           is_string: false,
                       default_value: false),

    FastlaneCore::ConfigItem.new(key: :group,
                            env_name: "APPCENTER_DISTRIBUTE_GROUP",
                         description: "Comma separated list of Distribution Group names",
                            optional: true,
                                type: String,
                          deprecated: true,
                        verify_block: proc do |value|
                          UI.user_error!("Option `group` is deprecated. Use `destinations` and `destination_type`")
                        end),

    FastlaneCore::ConfigItem.new(key: :destinations,
                            env_name: "APPCENTER_DISTRIBUTE_DESTINATIONS",
                         description: "Comma separated list of destination names, use '*' for all distribution groups if destination type is 'group'. Both distribution groups and stores are supported. All names are required to be of the same destination type",
                       default_value: Actions.lane_context[SharedValues::APPCENTER_DISTRIBUTE_DESTINATIONS] || "Collaborators",
                            optional: true,
                                type: String),

    FastlaneCore::ConfigItem.new(key: :destination_type,
                            env_name: "APPCENTER_DISTRIBUTE_DESTINATION_TYPE",
                         description: "Destination type of distribution destination. 'group' and 'store' are supported",
                       default_value: "group",
                            optional: true,
                                type: String,
                        verify_block: proc do |value|
                          UI.user_error!("No or incorrect destination type given. Use 'group' or 'store'") unless value && !value.empty? && ["group", "store"].include?(value)
                        end),

    FastlaneCore::ConfigItem.new(key: :mandatory_update,
                            env_name: "APPCENTER_DISTRIBUTE_MANDATORY_UPDATE",
                         description: "Require users to update to this release. Ignored if destination type is 'store'",
                            optional: true,
                           is_string: false,
                       default_value: false),

    FastlaneCore::ConfigItem.new(key: :notify_testers,
                            env_name: "APPCENTER_DISTRIBUTE_NOTIFY_TESTERS",
                         description: "Send email notification about release. Ignored if destination type is 'store'",
                            optional: true,
                           is_string: false,
                       default_value: false),

    FastlaneCore::ConfigItem.new(key: :release_notes,
                            env_name: "APPCENTER_DISTRIBUTE_RELEASE_NOTES",
                         description: "Release notes",
                       default_value: Actions.lane_context[SharedValues::FL_CHANGELOG] || "No changelog given",
                            optional: true,
                                type: String),

    FastlaneCore::ConfigItem.new(key: :should_clip,
                            env_name: "APPCENTER_DISTRIBUTE_RELEASE_NOTES_CLIPPING",
                         description: "Clip release notes if its length is more then #{Constants::MAX_RELEASE_NOTES_LENGTH}, true by default",
                            optional: true,
                           is_string: false,
                       default_value: true),

    FastlaneCore::ConfigItem.new(key: :release_notes_link,
                            env_name: "APPCENTER_DISTRIBUTE_RELEASE_NOTES_LINK",
                         description: "Additional release notes link",
                            optional: true,
                                type: String),

    FastlaneCore::ConfigItem.new(key: :build_number,
                                 env_name: "APPCENTER_DISTRIBUTE_BUILD_NUMBER",
                                 description: "The build number, required for macOS .pkg and .dmg builds, as well as Android ProGuard `mapping.txt` when using `upload_mapping_only`",
                                 optional: true,
                                 type: String),

    FastlaneCore::ConfigItem.new(key: :version,
                                 env_name: "APPCENTER_DISTRIBUTE_VERSION",
                                 description: "The build version, required for .pkg, .dmg, .zip and .msi builds, as well as Android ProGuard `mapping.txt` when using `upload_mapping_only`",
                                 optional: true,
                                 type: String),

    FastlaneCore::ConfigItem.new(key: :timeout,
                                 env_name: "APPCENTER_DISTRIBUTE_TIMEOUT",
                                 description: "Request timeout in seconds applied to individual HTTP requests. Some commands use multiple HTTP requests, large file uploads are also split in multiple HTTP requests",
                                 optional: true,
                                 type: Integer),

    FastlaneCore::ConfigItem.new(key: :dsa_signature,
                                 env_name: "APPCENTER_DISTRIBUTE_DSA_SIGNATURE",
                                 description: "DSA signature of the macOS or Windows release for Sparkle update feed",
                                 optional: true,
                                 type: String),
    
    FastlaneCore::ConfigItem.new(key: :ed_signature,
                                 env_name: "APPCENTER_DISTRIBUTE_ED_SIGNATURE",
                                 description: "EdDSA signature of the macOS or Windows release for Sparkle update feed",
                                 optional: true,
                                 type: String),
    
    FastlaneCore::ConfigItem.new(key: :strict,
                                 env_name: "APPCENTER_STRICT_MODE",
                                 description: "Strict mode, set to 'true' to fail early in case a potential error was detected",
                                 optional: true,
                                 type: String)
  ]
end
description() click to toggle source
# File lib/fastlane/plugin/appcenter/actions/appcenter_upload_action.rb, line 366
def self.description
  "Distribute new release to App Center"
end
details() click to toggle source
# File lib/fastlane/plugin/appcenter/actions/appcenter_upload_action.rb, line 374
def self.details
  "Symbols will also be uploaded automatically if a `app.dSYM.zip` file is found next to `app.ipa`. In case it is located in a different place you can specify the path explicitly in `:dsym` parameter."
end
example_code() click to toggle source
# File lib/fastlane/plugin/appcenter/actions/appcenter_upload_action.rb, line 675
def self.example_code
  [
    'appcenter_upload(
      api_token: "...",
      owner_name: "appcenter_owner",
      app_name: "testing_android_app",
      file: "./app-release.apk",
      destinations: "Testers",
      destination_type: "group",
      mapping: "./mapping.txt",
      release_notes: "release notes",
      notify_testers: false
    )',
    'appcenter_upload(
      api_token: "...",
      owner_name: "appcenter_owner",
      app_name: "testing_ios_app",
      file: "./app-release.ipa",
      destinations: "Testers,Public",
      destination_type: "group",
      dsym: "./app.dSYM.zip",
      release_notes: "release notes",
      notify_testers: false
    )',
    'appcenter_upload(
      api_token: "...",
      owner_name: "appcenter_owner",
      app_name: "testing_ios_app",
      file: "./app-release.ipa",
      destinations: "*",
      destination_type: "group",
      release_notes: "release notes",
      notify_testers: false
    )',
    'appcenter_upload(
      api_token: "...",
      owner_name: "appcenter_owner",
      app_name: "testing_google_play_app",
      file: "./app.aab",
      destinations: "Alpha",
      destination_type: "store",
      release_notes: "this is a store release"
    )'
  ]
end
get_or_create_app(params) click to toggle source

checks app existance, if ther is no such - creates it

# File lib/fastlane/plugin/appcenter/actions/appcenter_upload_action.rb, line 278
def self.get_or_create_app(params)
  api_token = params[:api_token]
  owner_type = params[:owner_type]
  owner_name = params[:owner_name]
  app_name = params[:app_name]
  app_display_name = params[:app_display_name]
  app_os = params[:app_os]
  app_platform = params[:app_platform]

  platforms = {
    Android: %w[Java React-Native Xamarin Unity],
    iOS: %w[Objective-C-Swift React-Native Xamarin Unity],
    macOS: %w[Objective-C-Swift],
    Windows: %w[UWP WPF WinForms Unity],
    Custom: %w[Custom]
  }

  begin
    if Helper::AppcenterHelper.get_app(api_token, owner_name, app_name)
      return true
    end
  rescue URI::InvalidURIError
    UI.user_error!("Provided app_name: '#{app_name}' is not in a valid format. Please ensure no special characters or spaces in the app_name.")
    return false
  end

  should_create_app = !app_display_name.to_s.empty? || !app_os.to_s.empty? || !app_platform.to_s.empty?

  if Helper.test? || should_create_app || UI.confirm("App with name #{app_name} not found, create one?")
    app_display_name = app_name if app_display_name.to_s.empty?
    os = app_os.to_s.empty? && (Helper.test? ? "Android" : UI.select("Select OS", platforms.keys)) || app_os.to_s
    platform = app_platform.to_s.empty? && (Helper.test? ? platforms[os.to_sym][0] : app_platform.to_s) || app_platform.to_s
    if platform.to_s.empty?
      platform = platforms[os.to_sym].length == 1 ? platforms[os.to_sym][0] : UI.select("Select Platform", platforms[os.to_sym])
    end

    Helper::AppcenterHelper.create_app(api_token, owner_type, owner_name, app_name, app_display_name, os, platform)
  else
    UI.error("Lane aborted")
    false
  end
end
is_apple_build(file) click to toggle source
# File lib/fastlane/plugin/appcenter/actions/appcenter_upload_action.rb, line 42
def self.is_apple_build(file)
  return false unless file

  file_ext = Helper::AppcenterHelper.file_extname_full(file)
  ((Constants::SUPPORTED_EXTENSIONS[:ios] + Constants::SUPPORTED_EXTENSIONS[:mac])).include? file_ext
end
is_supported?(platform) click to toggle source
# File lib/fastlane/plugin/appcenter/actions/appcenter_upload_action.rb, line 669
def self.is_supported?(platform)
  return Constants::SUPPORTED_EXTENSIONS.keys.include?(platform) if Options.strict

  true
end
optional_error(message) click to toggle source
# File lib/fastlane/plugin/appcenter/actions/appcenter_upload_action.rb, line 734
def self.optional_error(message)
  if Options.strict
    UI.user_error!(message)
  else
    UI.important(message)
    UI.important("The current operation might fail, trying anyway...")
  end
end
output() click to toggle source
# File lib/fastlane/plugin/appcenter/actions/appcenter_upload_action.rb, line 662
def self.output
  [
    ['APPCENTER_DOWNLOAD_LINK', 'The newly generated download link for this build'],
    ['APPCENTER_BUILD_INFORMATION', 'contains all keys/values from the App Center API']
  ]
end
run(params) click to toggle source
# File lib/fastlane/plugin/appcenter/actions/appcenter_upload_action.rb, line 345
def self.run(params)
  values = params.values
  upload_build_only = params[:upload_build_only]
  upload_dsym_only = params[:upload_dsym_only]
  upload_mapping_only = params[:upload_mapping_only]

  Options.strict_mode(params[:strict])

  # if app found or successfully created
  if self.get_or_create_app(params)
    self.add_app_to_distribution_group_if_needed(params)
    release = self.run_release_upload(params) unless upload_dsym_only || upload_mapping_only
    params[:version] = release['short_version'] if release
    params[:build_number] = release['version'] if release
    self.run_dsym_upload(params) unless upload_mapping_only || upload_build_only
    self.run_mapping_upload(params) unless upload_dsym_only || upload_build_only
  end

  return values if Helper.test?
end
run_dsym_upload(params) click to toggle source

run whole upload process for dSYM files

# File lib/fastlane/plugin/appcenter/actions/appcenter_upload_action.rb, line 50
def self.run_dsym_upload(params)
  values = params.values
  api_token = params[:api_token]
  owner_name = params[:owner_name]
  app_name = params[:app_name]
  file = params[:file] || params[:ipa]
  dsym = params[:dsym]
  build_number = params[:build_number]
  version = params[:version]

  dsym_path = nil
  if dsym
    # we can use dsym parameter for all apple builds
    self.optional_error("dsym parameter can only be used with Apple builds (ios, mac)") unless !file || self.is_apple_build(file)
    dsym_path = dsym
  else
    # if dsym is not set, but build is ipa - check default path
    if file && File.exist?(file) && File.extname(file) == '.ipa'
      dsym_path = file.to_s.gsub('.ipa', '.dSYM.zip')
      UI.message("dSYM is found")
    end
  end

  # if we provided valid dsym path, or <ipa_path>.dSYM.zip was found, start dSYM upload
  if dsym_path && File.exist?(dsym_path)
    if File.directory?(dsym_path)
      UI.message("dSYM path is folder, zipping...")
      dsym_path = Actions::ZipAction.run(path: dsym, output_path: dsym + ".zip")
      UI.message("dSYM files zipped")
    end

    values[:dsym_path] = dsym_path

    UI.message("Starting dSYM upload...")

    # TODO: this should eventually be removed once we have warned of deprecation for long enough
    if File.extname(dsym_path) == ".txt"
      file_name = File.basename(dsym_path)
      dsym_upload_details = Helper::AppcenterHelper.create_mapping_upload(api_token, owner_name, app_name, file_name ,build_number, version)
    else
      dsym_upload_details = Helper::AppcenterHelper.create_dsym_upload(api_token, owner_name, app_name)
    end

    if dsym_upload_details
      symbol_upload_id = dsym_upload_details['symbol_upload_id']
      upload_url = dsym_upload_details['upload_url']

      UI.message("Uploading dSYM...")
      Helper::AppcenterHelper.upload_symbol(api_token, owner_name, app_name, dsym_path, "Apple", symbol_upload_id, upload_url)
    end
  end
end
run_mapping_upload(params) click to toggle source
# File lib/fastlane/plugin/appcenter/actions/appcenter_upload_action.rb, line 103
def self.run_mapping_upload(params)
  api_token = params[:api_token]
  owner_name = params[:owner_name]
  app_name = params[:app_name]
  mapping = params[:mapping]
  build_number = params[:build_number]
  version = params[:version]

  if mapping == nil
    return
  end

  UI.message("Starting mapping upload...")
  mapping_name = File.basename(mapping)
  symbol_upload_details = Helper::AppcenterHelper.create_mapping_upload(api_token, owner_name, app_name, mapping_name, build_number, version)

  if symbol_upload_details
    symbol_upload_id = symbol_upload_details['symbol_upload_id']
    upload_url = symbol_upload_details['upload_url']

    UI.message("Uploading mapping...")
    Helper::AppcenterHelper.upload_symbol(api_token, owner_name, app_name, mapping, "Android", symbol_upload_id, upload_url)
  end
end
run_release_upload(params) click to toggle source

run whole upload process for release

# File lib/fastlane/plugin/appcenter/actions/appcenter_upload_action.rb, line 129
def self.run_release_upload(params)
  values = params.values
  api_token = params[:api_token]
  owner_name = params[:owner_name]
  owner_type = params[:owner_type]
  app_name = params[:app_name]
  destinations = params[:destinations]
  destination_type = params[:destination_type]
  mandatory_update = params[:mandatory_update]
  notify_testers = params[:notify_testers]
  release_notes = params[:release_notes]
  should_clip = params[:should_clip]
  release_notes_link = params[:release_notes_link]
  timeout = params[:timeout]
  build_number = params[:build_number]
  version = params[:version]
  dsa_signature = params[:dsa_signature]
  ed_signature = params[:ed_signature]

  if release_notes.length >= Constants::MAX_RELEASE_NOTES_LENGTH
    unless should_clip
      clip = UI.confirm("The release notes are limited to #{Constants::MAX_RELEASE_NOTES_LENGTH} characters, proceeding will clip them. Proceed anyway?")
      UI.abort_with_message!("Upload aborted, please edit your release notes") unless clip
      release_notes_link ||= UI.input("Provide a link for additional release notes, leave blank to skip")
    end
    read_more = "..." + (release_notes_link.to_s.empty? ? "" : "\n\n[read more](#{release_notes_link})")
    release_notes = release_notes[0, Constants::MAX_RELEASE_NOTES_LENGTH - read_more.length] + read_more
    values[:release_notes] = release_notes
    UI.message("Release notes clipped")
  end

  file = [
    params[:file],
    params[:ipa],
    params[:apk],
    params[:aab],
  ].detect { |e| !e.to_s.empty? }

  UI.user_error!("Couldn't find build file at path '#{file}'") unless file && File.exist?(file)

  file_ext = Helper::AppcenterHelper.file_extname_full(file)
  if destination_type == "group"
    self.optional_error("Can't distribute #{file_ext} to groups, please use `destination_type: 'store'`") if Constants::STORE_ONLY_EXTENSIONS.include? file_ext
  else
    self.optional_error("Can't distribute #{file_ext} to stores, please use `destination_type: 'group'`") unless Constants::STORE_SUPPORTED_EXTENSIONS.include? file_ext
    UI.user_error!("The combination of `destinations: '*'` and `destination_type: 'store'` is invalid, please use `destination_type: 'group'` or explicitly specify the destinations") if destinations == "*"
  end

  release_upload_body = nil
  unless params[:file].to_s.empty?
    if Constants::FULL_VERSION_REQUIRED_EXTENSIONS.include? file_ext
      self.optional_error("Fields `version` and `build_number` must be specified to upload a #{file_ext} file") if build_number.to_s.empty? || version.to_s.empty?
    elsif Constants::VERSION_REQUIRED_EXTENSIONS.include? file_ext
      self.optional_error("Field `version` must be specified to upload a #{file_ext} file") if version.to_s.empty?
    else
      self.optional_error("Fields `version` and `build_number` are not supported for files of type #{file_ext}") unless build_number.to_s.empty? && version.to_s.empty?
    end

    release_upload_body = { build_version: version } unless version.nil?
    release_upload_body = { build_version: version, build_number: build_number } if !version.nil? && !build_number.nil?
  end

  if file_ext == ".app" && File.directory?(file)
    UI.message("App path is a directory, zipping it before upload")
    zip_file = file + ".zip"
    if File.exist? zip_file
      override = UI.interactive? ? UI.confirm("File '#{zip_file}' already exists, do you want to override it?") : true
      UI.abort_with_message!("Not overriding, aborting publishing operation") unless override
      UI.message("Deleting zip archive: #{zip_file}")
      File.delete zip_file
    end
    UI.message("Creating zip archive: #{zip_file}")
    file = Actions::ZipAction.run(path: file, output_path: zip_file, symlinks: true)
  end

  UI.message("Starting release upload...")
  upload_details = Helper::AppcenterHelper.create_release_upload(api_token, owner_name, app_name, release_upload_body)
  if upload_details
    upload_id = upload_details['id']
    
    UI.message("Setting Metadata...")
    content_type = Constants::CONTENT_TYPES[File.extname(file)&.delete('.').downcase.to_sym] || "application/octet-stream"
    set_metadata_url = "#{upload_details['upload_domain']}/upload/set_metadata/#{upload_details['package_asset_id']}?file_name=#{File.basename(file)}&file_size=#{File.size(file)}&token=#{upload_details['url_encoded_token']}&content_type=#{content_type}"
    chunk_size = Helper::AppcenterHelper.set_release_upload_metadata(set_metadata_url, api_token, owner_name, app_name, upload_id, timeout)
    UI.abort_with_message!("Upload aborted") unless chunk_size

    UI.message("Uploading release binary...")
    upload_url = "#{upload_details['upload_domain']}/upload/upload_chunk/#{upload_details['package_asset_id']}?token=#{upload_details['url_encoded_token']}"
    uploaded = Helper::AppcenterHelper.upload_build(api_token, owner_name, app_name, file, upload_id, upload_url, content_type, chunk_size, timeout)
    UI.abort_with_message!("Upload aborted") unless uploaded

    UI.message("Finishing release...")
    finish_url = "#{upload_details['upload_domain']}/upload/finished/#{upload_details['package_asset_id']}?token=#{upload_details['url_encoded_token']}"
    finished = Helper::AppcenterHelper.finish_release_upload(finish_url, api_token, owner_name, app_name, upload_id, timeout)
    UI.abort_with_message!("Upload aborted") unless finished

    UI.message("Waiting for release to be ready...")
    release_status_url = "v0.1/apps/#{owner_name}/#{app_name}/uploads/releases/#{upload_id}"
    release_id = Helper::AppcenterHelper.poll_for_release_id(api_token, release_status_url)

    if release_id.is_a? Integer
      release_url = Helper::AppcenterHelper.get_release_url(owner_type, owner_name, app_name, release_id)
      UI.message("Release '#{release_id}' committed: #{release_url}")

      release = Helper::AppcenterHelper.update_release(api_token, owner_name, app_name, release_id, release_notes)
      Helper::AppcenterHelper.update_release_metadata(api_token, owner_name, app_name, release_id, dsa_signature, ed_signature)

      destinations_array = []
      if destinations == '*'
        UI.message("Looking up all distribution groups for #{owner_name}/#{app_name}")
        distribution_groups = Helper::AppcenterHelper.fetch_distribution_groups(
          api_token: api_token,
          owner_name: owner_name,
          app_name: app_name
        )

        UI.abort_with_message!("Failed to list distribution groups for #{owner_name}/#{app_name}") unless distribution_groups
        
        destinations_array = distribution_groups.map {|h| h['name'] }
      else
        destinations_array = destinations.split(',').map(&:strip)
      end
      
      destinations_array.each do |destination_name|
        destination = Helper::AppcenterHelper.get_destination(api_token, owner_name, app_name, destination_type, destination_name)
        if destination
          destination_id = destination['id']
          distributed_release = Helper::AppcenterHelper.add_to_destination(api_token, owner_name, app_name, release_id, destination_type, destination_id, mandatory_update, notify_testers)
          if distributed_release
            UI.success("Release '#{release_id}' (#{distributed_release['short_version']}) was successfully distributed to #{destination_type} \"#{destination_name}\"")
          else
            UI.error("Release '#{release_id}' was not found for destination '#{destination_name}'")
          end
        else
          UI.error("#{destination_type} '#{destination_name}' was not found")
        end
      end

      safe_download_url = Helper::AppcenterHelper.get_install_url(owner_type, owner_name, app_name)
      UI.message("Release '#{release_id}' is available for download at: #{safe_download_url}")
    else
      UI.user_error!("Failed to upload release")
    end
  end

  release
end