class Fastlane::Actions::MobileCenterUploadAction

Public Class Methods

add_to_group(api_token, release_url, group_name, release_notes = '') click to toggle source

add release to distribution group

# File lib/fastlane/plugin/mobile_center/actions/mobile_center_upload_action.rb, line 195
def self.add_to_group(api_token, release_url, group_name, release_notes = '')
  connection = self.connection

  response = connection.patch do |req|
    req.url("/#{release_url}")
    req.headers['X-API-Token'] = api_token
    req.headers['internal-request-source'] = "fastlane"
    req.body = {
      "distribution_group_name" => group_name,
      "release_notes" => release_notes
    }
  end

  case response.status
  when 200...300
    release = response.body
    download_url = release['download_url']

    UI.message("DEBUG: #{JSON.pretty_generate(release)}") if ENV['DEBUG']

    Actions.lane_context[SharedValues::MOBILE_CENTER_DOWNLOAD_LINK] = download_url
    Actions.lane_context[SharedValues::MOBILE_CENTER_BUILD_INFORMATION] = release

    UI.message("Public Download URL: #{download_url}") if download_url
    UI.success("Release #{release['short_version']} was successfully distributed")

    release
  when 404
    UI.error("Not found, invalid distribution group name")
    false
  else
    UI.error("Error adding to group #{response.status}: #{response.body}")
    false
  end
end
authors() click to toggle source
# File lib/fastlane/plugin/mobile_center/actions/mobile_center_upload_action.rb, line 396
def self.authors
  ["Microsoft"]
end
available_options() click to toggle source
# File lib/fastlane/plugin/mobile_center/actions/mobile_center_upload_action.rb, line 406
def self.available_options
  [
    FastlaneCore::ConfigItem.new(key: :api_token,
                            env_name: "MOBILE_CENTER_API_TOKEN",
                         description: "API Token for Mobile Center",
                            optional: false,
                                type: String,
                        verify_block: proc do |value|
                          UI.user_error!("No API token for Mobile Center given, pass using `api_token: 'token'`") unless value && !value.empty?
                        end),

    FastlaneCore::ConfigItem.new(key: :owner_name,
                            env_name: "MOBILE_CENTER_OWNER_NAME",
                         description: "Owner name",
                            optional: false,
                                type: String,
                        verify_block: proc do |value|
                          UI.user_error!("No Owner name for Mobile Center given, pass using `owner_name: 'name'`") unless value && !value.empty?
                        end),

    FastlaneCore::ConfigItem.new(key: :app_name,
                            env_name: "MOBILE_CENTER_APP_NAME",
                         description: "App name. If there is no app with such name, you will be prompted to create one",
                            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: :apk,
                            env_name: "MOBILE_CENTER_DISTRIBUTE_APK",
                         description: "Build release path for android build",
                       default_value: Actions.lane_context[SharedValues::GRADLE_APK_OUTPUT_PATH],
                            optional: true,
                                type: String,
                 conflicting_options: [:ipa],
                      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"]
                          UI.user_error!("Only \".apk\" formats are allowed, you provided \"#{File.extname(value)}\"") unless accepted_formats.include? File.extname(value)
                        end),

    FastlaneCore::ConfigItem.new(key: :ipa,
                            env_name: "MOBILE_CENTER_DISTRIBUTE_IPA",
                         description: "Build release path for ios build",
                       default_value: Actions.lane_context[SharedValues::IPA_OUTPUT_PATH],
                            optional: true,
                                type: String,
                 conflicting_options: [:apk],
                      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"]
                          UI.user_error!("Only \".ipa\" formats are allowed, you provided \"#{File.extname(value)}\"") unless accepted_formats.include? File.extname(value)
                        end),

    FastlaneCore::ConfigItem.new(key: :dsym,
                            env_name: "MOBILE_CENTER_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|
                          if value
                            UI.user_error!("Couldn't find dSYM file at path '#{value}'") unless File.exist?(value)
                          end
                        end),

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

    FastlaneCore::ConfigItem.new(key: :group,
                            env_name: "MOBILE_CENTER_DISTRIBUTE_GROUP",
                         description: "Distribute group name",
                       default_value: "Collaborators",
                            optional: true,
                                type: String),

    FastlaneCore::ConfigItem.new(key: :release_notes,
                            env_name: "MOBILE_CENTER_DISTRIBUTE_RELEASE_NOTES",
                         description: "Release notes",
                       default_value: Actions.lane_context[SharedValues::FL_CHANGELOG] || "No changelog given",
                            optional: true,
                                type: String)
  ]
end
connection(upload_url = false, dsym = false) click to toggle source

create request

# File lib/fastlane/plugin/mobile_center/actions/mobile_center_upload_action.rb, line 11
def self.connection(upload_url = false, dsym = false)
  require 'faraday'
  require 'faraday_middleware'

  options = {
    url: upload_url ? upload_url : "https://api.mobile.azure.com"
  }

  Faraday.new(options) do |builder|
    if upload_url
      builder.request :multipart unless dsym
      builder.request :url_encoded unless dsym
    else
      builder.request :json
    end
    builder.response :json, content_type: /\bjson$/
    builder.use FaradayMiddleware::FollowRedirects
    builder.adapter :net_http
  end
end
create_dsym_upload(api_token, owner_name, app_name) click to toggle source

creates new dSYM upload in mobile center returns: symbol_upload_id upload_url expiration_date

# File lib/fastlane/plugin/mobile_center/actions/mobile_center_upload_action.rb, line 67
def self.create_dsym_upload(api_token, owner_name, app_name)
  connection = self.connection

  response = connection.post do |req|
    req.url("/v0.1/apps/#{owner_name}/#{app_name}/symbol_uploads")
    req.headers['X-API-Token'] = api_token
    req.headers['internal-request-source'] = "fastlane"
    req.body = {
      symbol_type: 'Apple'
    }
  end

  case response.status
  when 200...300
    UI.message("DEBUG: #{JSON.pretty_generate(response.body)}\n") if ENV['DEBUG']
    response.body
  when 401
    UI.user_error!("Auth Error, provided invalid token")
    false
  when 404
    UI.error("Not found, invalid owner or application name")
    false
  else
    UI.error("Error #{response.status}: #{response.body}")
    false
  end
end
create_release_upload(api_token, owner_name, app_name) click to toggle source

creates new release upload returns: upload_id upload_url

# File lib/fastlane/plugin/mobile_center/actions/mobile_center_upload_action.rb, line 36
def self.create_release_upload(api_token, owner_name, app_name)
  connection = self.connection

  response = connection.post do |req|
    req.url("/v0.1/apps/#{owner_name}/#{app_name}/release_uploads")
    req.headers['X-API-Token'] = api_token
    req.headers['internal-request-source'] = "fastlane"
    req.body = {}
  end

  case response.status
  when 200...300
    UI.message("DEBUG: #{JSON.pretty_generate(response.body)}\n") if ENV['DEBUG']
    response.body
  when 401
    UI.user_error!("Auth Error, provided invalid token")
    false
  when 404
    UI.error("Not found, invalid owner or application name")
    false
  else
    UI.error("Error #{response.status}: #{response.body}")
    false
  end
end
description() click to toggle source
# File lib/fastlane/plugin/mobile_center/actions/mobile_center_upload_action.rb, line 392
def self.description
  "Distribute new release to Mobile Center"
end
details() click to toggle source
# File lib/fastlane/plugin/mobile_center/actions/mobile_center_upload_action.rb, line 400
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/mobile_center/actions/mobile_center_upload_action.rb, line 511
def self.example_code
  [
    'mobile_center_upload(
      api_token: "...",
      owner_name: "mobile_center_owner",
      app_name: "testing_app",
      apk: "./app-release.apk",
      group: "Testers",
      release_notes: "release notes"
    )',
    'mobile_center_upload(
      api_token: "...",
      owner_name: "mobile_center_owner",
      app_name: "testing_app",
      apk: "./app-release.ipa",
      group: "Testers",
      dsym: "./app.dSYM.zip",
      release_notes: "release notes"
    )'
  ]
end
get_app(api_token, owner_name, app_name) click to toggle source

returns true if app exists, false in case of 404 and error otherwise

# File lib/fastlane/plugin/mobile_center/actions/mobile_center_upload_action.rb, line 308
def self.get_app(api_token, owner_name, app_name)
  connection = self.connection

  response = connection.get do |req|
    req.url("/v0.1/apps/#{owner_name}/#{app_name}")
    req.headers['X-API-Token'] = api_token
    req.headers['internal-request-source'] = "fastlane"
  end

  case response.status
  when 200...300
    UI.message("DEBUG: #{JSON.pretty_generate(response.body)}\n") if ENV['DEBUG']
    true
  when 404
    UI.message("DEBUG: #{JSON.pretty_generate(response.body)}\n") if ENV['DEBUG']
    false
  else
    UI.error("Error #{response.status}: #{response.body}")
    false
  end
end
get_or_create_app(params) click to toggle source

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

# File lib/fastlane/plugin/mobile_center/actions/mobile_center_upload_action.rb, line 331
def self.get_or_create_app(params)
  api_token = params[:api_token]
  owner_name = params[:owner_name]
  app_name = params[:app_name]

  platforms = {
    "Android" => ['Java', 'React-Native', 'Xamarin'],
    "iOS" => ['Objective-C-Swift', 'React-Native', 'Xamarin']
  }

  if self.get_app(api_token, owner_name, app_name)
    true
  else
    if Helper.test? || UI.confirm("App with name #{app_name} not found, create one?")
      connection = self.connection

      os = Helper.test? ? "Android" : UI.select("Select OS", ["Android", "iOS"])
      platform = Helper.test? ? "Java" : UI.select("Select Platform", platforms[os])

      response = connection.post do |req|
        req.url("/v0.1/apps")
        req.headers['X-API-Token'] = api_token
        req.headers['internal-request-source'] = "fastlane"
        req.body = {
          "display_name" => app_name,
          "name" => app_name,
          "os" => os,
          "platform" => platform
        }
      end

      case response.status
      when 200...300
        created = response.body
        UI.message("DEBUG: #{JSON.pretty_generate(created)}") if ENV['DEBUG']
        UI.success("Created #{os}/#{platform} app with name \"#{created['name']}\"")
        true
      else
        UI.error("Error creating app #{response.status}: #{response.body}")
        false
      end
    else
      UI.error("Lane aborted")
      false
    end
  end
end
is_supported?(platform) click to toggle source
# File lib/fastlane/plugin/mobile_center/actions/mobile_center_upload_action.rb, line 507
def self.is_supported?(platform)
  true
end
output() click to toggle source
# File lib/fastlane/plugin/mobile_center/actions/mobile_center_upload_action.rb, line 500
def self.output
  [
    ['MOBILE_CENTER_DOWNLOAD_LINK', 'The newly generated download link for this build'],
    ['MOBILE_CENTER_BUILD_INFORMATION', 'contains all keys/values from the Mobile CEnter API']
  ]
end
run(params) click to toggle source
# File lib/fastlane/plugin/mobile_center/actions/mobile_center_upload_action.rb, line 379
def self.run(params)
  values = params.values
  upload_dsym_only = params[:upload_dsym_only]

  # if app found or successfully created
  if self.get_or_create_app(params)
    self.run_release_upload(params) unless upload_dsym_only
    self.run_dsym_upload(params)
  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/mobile_center/actions/mobile_center_upload_action.rb, line 232
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[:ipa]
  dsym = params[:dsym]

  dsym_path = nil
  if dsym
    # we can use dsym parameter only if build file is ipa
    dsym_path = dsym if !file || File.extname(file) == '.ipa'
  else
    # if dsym is note 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...")
    dsym_upload_details = self.create_dsym_upload(api_token, owner_name, app_name)

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

      UI.message("Uploading dSYM...")
      self.upload_dsym(api_token, owner_name, app_name, dsym_path, symbol_upload_id, upload_url)
    end
  end
end
run_release_upload(params) click to toggle source

run whole upload process for release

# File lib/fastlane/plugin/mobile_center/actions/mobile_center_upload_action.rb, line 276
def self.run_release_upload(params)
  api_token = params[:api_token]
  owner_name = params[:owner_name]
  app_name = params[:app_name]
  group = params[:group]

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

  UI.user_error!("Couldn't find build file at path '#{file}'") unless file && File.exist?(file)
  UI.user_error!("No Distribute Group given, pass using `group: 'group name'`") unless group && !group.empty?

  UI.message("Starting release upload...")
  upload_details = self.create_release_upload(api_token, owner_name, app_name)
  if upload_details
    upload_id = upload_details['upload_id']
    upload_url = upload_details['upload_url']

    UI.message("Uploading release binary...")
    uploaded = self.upload_build(api_token, owner_name, app_name, file, upload_id, upload_url)

    if uploaded
      release_url = uploaded['release_url']
      UI.message("Release committed")
      self.add_to_group(api_token, release_url, group, params[:release_notes])
    end
  end
end
update_dsym_upload(api_token, owner_name, app_name, symbol_upload_id, status) click to toggle source

committs or aborts dsym upload

# File lib/fastlane/plugin/mobile_center/actions/mobile_center_upload_action.rb, line 96
def self.update_dsym_upload(api_token, owner_name, app_name, symbol_upload_id, status)
  connection = self.connection

  response = connection.patch do |req|
    req.url("/v0.1/apps/#{owner_name}/#{app_name}/symbol_uploads/#{symbol_upload_id}")
    req.headers['X-API-Token'] = api_token
    req.headers['internal-request-source'] = "fastlane"
    req.body = {
      "status" => status
    }
  end

  case response.status
  when 200...300
    UI.message("DEBUG: #{JSON.pretty_generate(response.body)}\n") if ENV['DEBUG']
    response.body
  else
    UI.error("Error #{response.status}: #{response.body}")
    false
  end
end
update_release_upload(api_token, owner_name, app_name, upload_id, status) click to toggle source

Commits or aborts the upload process for a release

# File lib/fastlane/plugin/mobile_center/actions/mobile_center_upload_action.rb, line 172
def self.update_release_upload(api_token, owner_name, app_name, upload_id, status)
  connection = self.connection

  response = connection.patch do |req|
    req.url("/v0.1/apps/#{owner_name}/#{app_name}/release_uploads/#{upload_id}")
    req.headers['X-API-Token'] = api_token
    req.headers['internal-request-source'] = "fastlane"
    req.body = {
      "status" => status
    }
  end

  case response.status
  when 200...300
    UI.message("DEBUG: #{JSON.pretty_generate(response.body)}\n") if ENV['DEBUG']
    response.body
  else
    UI.error("Error #{response.status}: #{response.body}")
    false
  end
end
upload_build(api_token, owner_name, app_name, file, upload_id, upload_url) click to toggle source

upload binary for specified upload_url if succeed, then commits the release otherwise aborts

# File lib/fastlane/plugin/mobile_center/actions/mobile_center_upload_action.rb, line 146
def self.upload_build(api_token, owner_name, app_name, file, upload_id, upload_url)
  connection = self.connection(upload_url)

  options = {}
  options[:upload_id] = upload_id
  # ipa field is used both for .apk and .ipa files
  options[:ipa] = Faraday::UploadIO.new(file, 'application/octet-stream') if file && File.exist?(file)

  response = connection.post do |req|
    req.headers['internal-request-source'] = "fastlane"
    req.body = options
  end

  case response.status
  when 200...300
    UI.message("Binary uploaded")
    self.update_release_upload(api_token, owner_name, app_name, upload_id, 'committed')
  else
    UI.error("Error uploading binary #{response.status}: #{response.body}")
    self.update_release_upload(api_token, owner_name, app_name, upload_id, 'aborted')
    UI.error("Release aborted")
    false
  end
end
upload_dsym(api_token, owner_name, app_name, dsym, symbol_upload_id, upload_url) click to toggle source

upload dSYM files to specified upload url if succeed, then commits the upload otherwise aborts

# File lib/fastlane/plugin/mobile_center/actions/mobile_center_upload_action.rb, line 121
def self.upload_dsym(api_token, owner_name, app_name, dsym, symbol_upload_id, upload_url)
  connection = self.connection(upload_url, true)

  response = connection.put do |req|
    req.headers['x-ms-blob-type'] = "BlockBlob"
    req.headers['Content-Length'] = File.size(dsym).to_s
    req.headers['internal-request-source'] = "fastlane"
    req.body = Faraday::UploadIO.new(dsym, 'application/octet-stream') if dsym && File.exist?(dsym)
  end

  case response.status
  when 200...300
    self.update_dsym_upload(api_token, owner_name, app_name, symbol_upload_id, 'committed')
    UI.success("dSYM uploaded")
  else
    UI.error("Error uploading dSYM #{response.status}: #{response.body}")
    self.update_dsym_upload(api_token, owner_name, app_name, symbol_upload_id, 'aborted')
    UI.error("dSYM upload aborted")
    false
  end
end