Class: Fastlane::Actions::SaucelabsAppdistAction

Inherits:
Action
  • Object
show all
Defined in:
lib/fastlane/plugin/saucelabs_appdist/actions/saucelabs_appdist_action.rb

Constant Summary collapse

LOCAL_ONLY_PARAMS =

Config that stays on this side of the wire.

%i[upload_url timeout api_version].freeze
PASSTHROUGH_PARAMS =

Params whose lane name is also the upload field name.

%i[
  api_key ipa apk symbols_file comment notify custom platform
  folder_name landing_page_mode app_description
].freeze
RECONCILED_PARAMS =

Params resolved as a pair or expanded into several fields.

%i[
  community_token landing_page_slug upload_to_saucelabs sync_to_saucelabs metadata
].freeze
IGNORED_BY_APPDIST =

Mobile App Distribution ignores these. Legacy TestFairy servers still read them, so they stay on the wire and only warn.

{
  testers_groups: 'Mobile App Distribution notifies every tester on the project; per-group notify is not available yet.',
  app_description: 'Set the landing page description from the dashboard.',
  metrics: 'Session metrics came from the TestFairy SDK, which Mobile App Distribution does not ship.',
  options: 'Session options came from the TestFairy SDK, which Mobile App Distribution does not ship.',
  custom: 'No equivalent.',
  auto_update: 'No equivalent.'
}.freeze
BINARY_EXTENSIONS =
%w(.ipa .apk .aab).freeze

Class Method Summary collapse

Class Method Details

.authors ⇒ Object



480
481
482
# File 'lib/fastlane/plugin/saucelabs_appdist/actions/saucelabs_appdist_action.rb', line 480

def self.authors
  ["Sauce Labs"]
end

.available_options ⇒ Object



267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
# File 'lib/fastlane/plugin/saucelabs_appdist/actions/saucelabs_appdist_action.rb', line 267

def self.available_options
  [
    # required
    FastlaneCore::ConfigItem.new(key: :api_key,
                                 env_name: "FL_SAUCELABS_APPDIST_API_KEY",
                                 description: "API Key for Sauce Labs App Distribution",
                                 sensitive: true,
                                 verify_block: proc do |value|
                                   UI.user_error!("No API key for Sauce Labs App Distribution given, pass using `api_key: 'key'`") unless value.to_s.length > 0
                                 end),
    FastlaneCore::ConfigItem.new(key: :ipa,
                                 env_name: 'SAUCELABS_APPDIST_IPA_PATH',
                                 description: 'Path to your IPA file for iOS',
                                 default_value: Actions.lane_context[SharedValues::IPA_OUTPUT_PATH],
                                 default_value_dynamic: true,
                                 optional: true,
                                 conflicting_options: [:apk],
                                 verify_block: proc do |value|
                                   UI.user_error!("Couldn't find ipa file at path '#{value}'") unless File.exist?(value)
                                 end),
    FastlaneCore::ConfigItem.new(key: :apk,
                                 env_name: 'SAUCELABS_APPDIST_APK_PATH',
                                 description: 'Path to your APK file for Android',
                                 default_value: Actions.lane_context[SharedValues::GRADLE_APK_OUTPUT_PATH],
                                 default_value_dynamic: true,
                                 optional: true,
                                 conflicting_options: [:ipa],
                                 verify_block: proc do |value|
                                   UI.user_error!("Couldn't find apk file at path '#{value}'") unless File.exist?(value)
                                 end),
    # optional
    FastlaneCore::ConfigItem.new(key: :symbols_file,
                                 optional: true,
                                 env_name: "FL_SAUCELABS_APPDIST_SYMBOLS_FILE",
                                 description: "Symbols mapping file",
                                 default_value: Actions.lane_context[SharedValues::DSYM_OUTPUT_PATH],
                                 default_value_dynamic: true,
                                 verify_block: proc do |value|
                                   UI.user_error!("Couldn't find dSYM file at path '#{value}'") unless File.exist?(value)
                                 end),
    FastlaneCore::ConfigItem.new(key: :upload_url,
                                 env_name: "FL_SAUCELABS_APPDIST_UPLOAD_URL",
                                 description: "API URL for Sauce Labs App Distribution",
                                 default_value: "https://app.testfairy.com",
                                 optional: true),
    FastlaneCore::ConfigItem.new(key: :api_version,
                                 optional: true,
                                 type: Symbol,
                                 env_name: "FL_SAUCELABS_APPDIST_API_VERSION",
                                 description: "Upload transport. This release supports :legacy (the /api/upload endpoint) only; :v3 arrives in plugin 3.x",
                                 default_value: :legacy,
                                 verify_block: proc do |value|
                                   if value.to_sym == :v3
                                     UI.user_error!("`api_version: :v3` needs plugin 3.x — this is #{Fastlane::SaucelabsAppdist::VERSION}, which uploads over the legacy /api/upload endpoint")
                                   end
                                   UI.user_error!("Unknown api_version `#{value}`. This release supports :legacy only") unless value.to_sym == :legacy
                                 end),
    FastlaneCore::ConfigItem.new(key: :team_id,
                                 optional: true,
                                 env_name: "FL_SAUCELABS_APPDIST_TEAM_ID",
                                 skip_type_validation: true,
                                 description: "Team that owns the app. Pass it when the same bundle id exists on more than one of your teams, otherwise the upload is refused with code 136",
                                 default_value: '',
                                 verify_block: proc do |value|
                                   UI.user_error!("The team_id must be a number") unless value.to_s.empty? || value.to_s.match?(/\A\d+\z/)
                                 end),
    FastlaneCore::ConfigItem.new(key: :testers_groups,
                                 optional: true,
                                 type: Array,
                                 short_option: '-g',
                                 env_name: "FL_SAUCELABS_APPDIST_TESTERS_GROUPS",
                                 description: "Array of tester groups to be notified. Legacy TestFairy only — Mobile App Distribution notifies every tester on the project",
                                 default_value: []),
    FastlaneCore::ConfigItem.new(key: :metrics,
                                 optional: true,
                                 type: Array,
                                 env_name: "FL_SAUCELABS_APPDIST_METRICS",
                                 description: "Array of metrics to record (cpu,memory,network,phone_signal,gps,battery,mic,wifi). Legacy TestFairy SDK only",
                                 default_value: []),
    FastlaneCore::ConfigItem.new(key: :comment,
                                 optional: true,
                                 env_name: "FL_SAUCELABS_APPDIST_COMMENT",
                                 description: "Additional release notes for this upload. This text will be added to email notifications",
                                 default_value: 'No comment provided'),
    FastlaneCore::ConfigItem.new(key: :auto_update,
                                 optional: true,
                                 env_name: "FL_SAUCELABS_APPDIST_AUTO_UPDATE",
                                 description: "Allows an easy upgrade of all users to the current version. To enable set to 'on'. Legacy TestFairy only",
                                 default_value: 'off'),
    FastlaneCore::ConfigItem.new(key: :notify,
                                 optional: true,
                                 env_name: "FL_SAUCELABS_APPDIST_NOTIFY",
                                 description: "Send email to testers. Can be 'on', 'off', '1' or '0'",
                                 default_value: 'off',
                                 verify_block: proc do |value|
                                   UI.user_error!("The notify flag can only be on, off, 1 or 0 — any other value silently notifies nobody") unless %w(on off 1 0).include?(value.to_s)
                                 end),
    FastlaneCore::ConfigItem.new(key: :options,
                                 optional: true,
                                 type: Array,
                                 env_name: "FL_SAUCELABS_APPDIST_OPTIONS",
                                 description: "Array of options (shake,video_only_wifi,anonymous). Legacy TestFairy SDK only",
                                 default_value: []),
    FastlaneCore::ConfigItem.new(key: :custom,
                                 optional: true,
                                 env_name: "FL_SAUCELABS_APPDIST_CUSTOM",
                                 description: "Array of custom options. Legacy TestFairy only — contact support for more information",
                                 default_value: ''),
    FastlaneCore::ConfigItem.new(key: :timeout,
                                 env_name: "FL_SAUCELABS_APPDIST_TIMEOUT",
                                 description: "Request timeout in seconds",
                                 type: Integer,
                                 optional: true),
    FastlaneCore::ConfigItem.new(key: :tags,
                                 optional: true,
                                 env_name: "FL_SAUCELABS_APPDIST_TAGS",
                                 description: "Custom tags that can be used to organize your builds",
                                 type: Array,
                                 default_value: []),
    FastlaneCore::ConfigItem.new(key: :metadata,
                                 optional: true,
                                 type: Hash,
                                 env_name: "FL_SAUCELABS_APPDIST_METADATA",
                                 description: "Arbitrary key-value pairs to store on the build. Each pair is sent as a metadata_<key> field and comes back under `metadata` in the response",
                                 default_value: {}),
    FastlaneCore::ConfigItem.new(key: :folder_name,
                                 optional: true,
                                 env_name: "FL_SAUCELABS_APPDIST_FOLDER_NAME",
                                 description: "Name of the dashboard folder that contains this app",
                                 default_value: ''),
    FastlaneCore::ConfigItem.new(key: :landing_page_mode,
                                 optional: true,
                                 env_name: "FL_SAUCELABS_APPDIST_LANDING_PAGE_MODE",
                                 description: "Visibility of build landing after upload. Can be 'open' or 'closed'",
                                 default_value: 'open',
                                 verify_block: proc do |value|
                                   UI.user_error!("The landing page mode can only be open or closed") unless %w(open closed).include?(value)
                                 end),
    FastlaneCore::ConfigItem.new(key: :sync_to_saucelabs,
                                 optional: true,
                                 env_name: "FL_SAUCELABS_APPDIST_SYNC_TO_SAUCELABS",
                                 description: "Also upload the file to Sauce Labs app storage. It can be 'on' or 'off'",
                                 default_value: 'off',
                                 verify_block: proc do |value|
                                   UI.user_error!("The sync to Sauce Labs can only be on or off") unless %w(on off).include?(value)
                                 end),
    FastlaneCore::ConfigItem.new(key: :upload_to_saucelabs,
                                 optional: true,
                                 deprecated: "Renamed to `sync_to_saucelabs`, which is the field Mobile App Distribution reads",
                                 env_name: "FL_SAUCELABS_APPDIST_UPLOAD_TO_SAUCELABS",
                                 description: "Upload file directly to Sauce Labs. It can be 'on' or 'off'",
                                 default_value: 'off',
                                 verify_block: proc do |value|
                                   UI.user_error!("The upload to Sauce Labs can only be on or off") unless %w(on off).include?(value)
                                 end),
    FastlaneCore::ConfigItem.new(key: :platform,
                                 optional: true,
                                 env_name: "FL_SAUCELABS_APPDIST_PLATFORM",
                                 description: "Platform of a generic upload. Ignored for .ipa, .apk and .aab, whose platform comes from the binary",
                                 default_value: ''),
    FastlaneCore::ConfigItem.new(key: :landing_page_slug,
                                 optional: true,
                                 env_name: "FL_SAUCELABS_APPDIST_LANDING_PAGE_SLUG",
                                 description: "Custom URL token for the landing page, served at /install/<slug>. 6-63 chars: letters, digits, dot, hyphen or underscore",
                                 default_value: ''),
    FastlaneCore::ConfigItem.new(key: :community_token,
                                 optional: true,
                                 env_name: "FL_SAUCELABS_APPDIST_COMMUNITY_TOKEN",
                                 description: "Legacy name for `landing_page_slug`",
                                 default_value: ''),
    FastlaneCore::ConfigItem.new(key: :app_description,
                                 optional: true,
                                 env_name: "FL_SAUCELABS_APPDIST_APP_DESCRIPTION",
                                 description: "Description text for the landing page. Legacy TestFairy only — Mobile App Distribution never writes it",
                                 default_value: '')
  ]
end

.binary_upload?(params) ⇒ Boolean

Returns:

  • (Boolean)


201
202
203
204
205
206
# File 'lib/fastlane/plugin/saucelabs_appdist/actions/saucelabs_appdist_action.rb', line 201

def self.binary_upload?(params)
  path = params[:ipa] || params[:apk]
  return false unless path

  BINARY_EXTENSIONS.include?(File.extname(path.to_s).downcase)
end

.category ⇒ Object



469
470
471
# File 'lib/fastlane/plugin/saucelabs_appdist/actions/saucelabs_appdist_action.rb', line 469

def self.category
  :beta
end

.client_options(params, given = provided_keys(params)) ⇒ Object

Build the upload fields. Every declared option is either mapped here or named in LOCAL_ONLY_PARAMS — an option added without a mapping fails loud instead of vanishing.



93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
# File 'lib/fastlane/plugin/saucelabs_appdist/actions/saucelabs_appdist_action.rb', line 93

def self.client_options(params, given = provided_keys(params))
  options = {}

  params.values.each_key do |key|
    value = params[key]

    case key
    when *LOCAL_ONLY_PARAMS, *RECONCILED_PARAMS
      next
    when *PASSTHROUGH_PARAMS
      options[key] = value
    when :team_id
      # 0.3.2 sent no team_id field at all, so don't start sending an
      # empty one: a server reading its presence as "filter by team"
      # would reject an upload that used to work.
      options[key] = value.to_s unless value.to_s.empty?
    when :testers_groups, :tags
      options[key] = Array(value).join(',')
    when :metrics
      options[key] = metrics_to_client(value).join(',')
    when :options
      options[key] = options_to_client(value).join(',')
    when :auto_update
      options['auto-update'] = value
    else
      UI.user_error!("Unknown parameter: #{key}")
    end
  end

  slug = landing_page_slug(params, given)
  options[:community_token] = slug
  options[:landing_page_slug] = slug

  sync = sync_to_saucelabs(params, given)
  options[:upload_to_saucelabs] = sync
  options[:sync_to_saucelabs] = sync

  options.merge((params[:metadata]))
end

.description ⇒ Object



259
260
261
# File 'lib/fastlane/plugin/saucelabs_appdist/actions/saucelabs_appdist_action.rb', line 259

def self.description
  'Upload a new build to Sauce Labs App Distribution'
end

.details ⇒ Object



263
264
265
# File 'lib/fastlane/plugin/saucelabs_appdist/actions/saucelabs_appdist_action.rb', line 263

def self.details
  'A Sauce Labs fastlane plugin for uploading iOS and Android builds to Mobile App Distribution (MAD).'
end

.example_code ⇒ Object



445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
# File 'lib/fastlane/plugin/saucelabs_appdist/actions/saucelabs_appdist_action.rb', line 445

def self.example_code
  [
    'saucelabs_appdist(
      api_key: "...",
      ipa: "./ipa_file.ipa",
      comment: "Build #{lane_context[SharedValues::BUILD_NUMBER]}",
    )',
    'saucelabs_appdist(
      api_key: "...",
      apk: "../build/app/outputs/apk/qa/release/app-qa-release.apk",
      comment: "Build #{lane_context[SharedValues::BUILD_NUMBER]}",
     )',
    '# Same bundle id on several teams: name the team so every upload
    # appends to the same app instead of forking a new one.
    saucelabs_appdist(
      api_key: "...",
      ipa: "./ipa_file.ipa",
      team_id: 42,
      landing_page_slug: "my-app-beta",
      metadata: { branch: "main", ci_build: "1234" },
    )'
  ]
end

.is_supported?(platform) ⇒ Boolean

Returns:

  • (Boolean)


484
485
486
# File 'lib/fastlane/plugin/saucelabs_appdist/actions/saucelabs_appdist_action.rb', line 484

def self.is_supported?(platform)
  [:ios, :android].include?(platform)
end

.landing_page_slug(params, given = provided_keys(params)) ⇒ Object

community_token is the legacy name for landing_page_slug.



149
150
151
152
153
154
155
156
157
158
# File 'lib/fastlane/plugin/saucelabs_appdist/actions/saucelabs_appdist_action.rb', line 149

def self.landing_page_slug(params, given = provided_keys(params))
  legacy = provided?(params, :community_token, given) ? params[:community_token] : nil
  current = provided?(params, :landing_page_slug, given) ? params[:landing_page_slug] : nil

  if legacy && current && legacy != current
    UI.user_error!("`community_token: '#{legacy}'` contradicts `landing_page_slug: '#{current}'`. Pass one of them.")
  end

  current || legacy || ''
end

.metadata_fields(metadata) ⇒ Object

Mobile App Distribution folds every metadata_<key> field into the build's metadata, so one hash entry becomes one field.



162
163
164
165
166
167
168
169
# File 'lib/fastlane/plugin/saucelabs_appdist/actions/saucelabs_appdist_action.rb', line 162

def self.()
  ( || {}).each_with_object({}) do |(key, value), fields|
    name = key.to_s.strip
    UI.user_error!("Metadata keys cannot be empty") if name.empty?

    fields["metadata_#{name}"] = value.to_s
  end
end

.metrics_to_client(metrics) ⇒ Object



208
209
210
211
212
213
214
215
216
217
218
219
# File 'lib/fastlane/plugin/saucelabs_appdist/actions/saucelabs_appdist_action.rb', line 208

def self.metrics_to_client(metrics)
  Array(metrics).map do |metric|
    case metric.to_sym
    when :cpu, :memory, :network, :gps, :battery, :mic, :wifi
      metric.to_s
    when :phone_signal
      'phone-signal'
    else
      UI.user_error!("Unknown metric: #{metric}")
    end
  end
end

.options_to_client(options) ⇒ Object



221
222
223
224
225
226
227
228
229
230
231
232
# File 'lib/fastlane/plugin/saucelabs_appdist/actions/saucelabs_appdist_action.rb', line 221

def self.options_to_client(options)
  Array(options).map do |option|
    case option.to_sym
    when :shake, :anonymous
      option.to_s
    when :video_only_wifi
      'video-only-wifi'
    else
      UI.user_error!("Unknown option: #{option}")
    end
  end
end

.output ⇒ Object



473
474
475
476
477
478
# File 'lib/fastlane/plugin/saucelabs_appdist/actions/saucelabs_appdist_action.rb', line 473

def self.output
  [
    ['SAUCELABS_APPDIST_UPLOAD_RESPONSE', 'Full response from the upload API'],
    ['SAUCELABS_APPDIST_UPLOAD_ERROR', 'Full error envelope (status, code, message) from a failed upload']
  ]
end

.provided?(params, key, given = provided_keys(params)) ⇒ Boolean

True when the lane passed the param and gave it a real value.

Returns:

  • (Boolean)


197
198
199
# File 'lib/fastlane/plugin/saucelabs_appdist/actions/saucelabs_appdist_action.rb', line 197

def self.provided?(params, key, given = provided_keys(params))
  given.include?(key) && ![nil, '', [], {}].include?(params[key])
end

.provided_keys(params) ⇒ Object

Keys the caller supplied, by lane argument or by env var.

Two channels, read separately on purpose. _values holds ONLY lane-passed values and must be read before params.values back-fills defaults into that same hash. Env values never enter _values at all — fastlane consults them later, inside fetch — so asking only _values makes every FL_SAUCELABS_APPDIST_* value read as absent.



190
191
192
193
194
# File 'lib/fastlane/plugin/saucelabs_appdist/actions/saucelabs_appdist_action.rb', line 190

def self.provided_keys(params)
  available_options.select do |option|
    params._values.key?(option.key) || !option.fetch_env_value.nil?
  end.map(&:key)
end

.run(params) ⇒ Object



69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
# File 'lib/fastlane/plugin/saucelabs_appdist/actions/saucelabs_appdist_action.rb', line 69

def self.run(params)
  UI.success('Starting with ipa upload to Sauce Labs App Distribution...')

  path = params[:ipa] || params[:apk]
  UI.user_error!("No ipa or apk were given") unless path

  # One snapshot, taken before `params.values` back-fills defaults into
  # the same hash it reads from.
  given = provided_keys(params)
  warn_ignored_params(params, given)
  client_options = self.client_options(params, given)

  return path if Helper.test?

  response = self.upload_build(params[:upload_url], path, client_options, params[:timeout])
  UI.user_error!(upload_error_message(response)) unless parse_response(response)

  UI.success("Build successfully uploaded to Sauce Labs App Distribution.")
  UI.success("Response:\n#{JSON.pretty_generate(Actions.lane_context[SharedValues::SAUCELABS_APPDIST_UPLOAD_RESPONSE])}")
end

.sync_to_saucelabs(params, given = provided_keys(params)) ⇒ Object

upload_to_saucelabs is the legacy field name; Mobile App Distribution reads sync_to_saucelabs. Both go out so one lane works on either.



135
136
137
138
139
140
141
142
143
144
145
146
# File 'lib/fastlane/plugin/saucelabs_appdist/actions/saucelabs_appdist_action.rb', line 135

def self.sync_to_saucelabs(params, given = provided_keys(params))
  legacy = provided?(params, :upload_to_saucelabs, given) ? params[:upload_to_saucelabs] : nil
  current = provided?(params, :sync_to_saucelabs, given) ? params[:sync_to_saucelabs] : nil

  if legacy && current && legacy != current
    UI.user_error!("`upload_to_saucelabs: '#{legacy}'` contradicts `sync_to_saucelabs: '#{current}'`. Keep `sync_to_saucelabs`.")
  end

  UI.important("`upload_to_saucelabs` is deprecated — rename it to `sync_to_saucelabs`.") if legacy

  current || legacy || params[:sync_to_saucelabs]
end

.upload_build(upload_url, ipa, options, timeout) ⇒ Object



36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
# File 'lib/fastlane/plugin/saucelabs_appdist/actions/saucelabs_appdist_action.rb', line 36

def self.upload_build(upload_url, ipa, options, timeout)
  require 'faraday'
  require 'faraday_middleware'

  UI.success("Uploading to #{upload_url}...")

  connection = Faraday.new(url: upload_url) do |builder|
    builder.request(:multipart)
    builder.request(:url_encoded)
    builder.request(:retry, max: 3, interval: 5)
    builder.response(:json, content_type: /\bjson$/)
    builder.use(FaradayMiddleware::FollowRedirects)
    builder.adapter(:net_http)
  end

  options[:file] = Faraday::UploadIO.new(ipa, 'application/octet-stream') if ipa && File.exist?(ipa)

  symbols_file = options.delete(:symbols_file)
  if symbols_file
    options[:symbols_file] = Faraday::UploadIO.new(symbols_file, 'application/octet-stream')
  end

  begin
    connection.post do |req|
      req.options.timeout = timeout
      req.url("/api/upload/")
      req.body = options
    end
  rescue Faraday::TimeoutError
    UI.crash!("Uploading build to Sauce Labs App Distribution timed out ⏳")
  end
end

.upload_error_message(response) ⇒ Object

Surface the API's own code and message so a lane can act on them, and keep the whole envelope in lane_context for retry logic.



236
237
238
239
240
241
242
243
244
245
# File 'lib/fastlane/plugin/saucelabs_appdist/actions/saucelabs_appdist_action.rb', line 236

def self.upload_error_message(response)
  body = response.body
  Actions.lane_context[SharedValues::SAUCELABS_APPDIST_UPLOAD_ERROR] = body

  if body.is_a?(Hash) && body['code']
    "Sauce Labs App Distribution upload failed with code #{body['code']}: #{body['message'] || '(no message)'}"
  else
    "Sauce Labs App Distribution upload failed with HTTP #{response.status}: #{body.inspect}"
  end
end

.warn_ignored_params(params, given = provided_keys(params)) ⇒ Object



171
172
173
174
175
176
177
178
179
180
181
# File 'lib/fastlane/plugin/saucelabs_appdist/actions/saucelabs_appdist_action.rb', line 171

def self.warn_ignored_params(params, given = provided_keys(params))
  IGNORED_BY_APPDIST.each do |key, alternative|
    next unless provided?(params, key, given)

    UI.important("Sauce Labs Mobile App Distribution ignores `#{key}`. #{alternative} Legacy TestFairy servers still honor it.")
  end

  return unless provided?(params, :platform, given) && binary_upload?(params)

  UI.important('Sauce Labs Mobile App Distribution ignores `platform` for .ipa, .apk and .aab uploads — it reads the platform from the binary. The param applies to generic uploads only.')
end