Class: Fastlane::Actions::SaucelabsAppdistAction
- Inherits:
-
Action
- Object
- Action
- Fastlane::Actions::SaucelabsAppdistAction
- 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
- .authors ⇒ Object
- .available_options ⇒ Object
- .binary_upload?(params) ⇒ Boolean
- .category ⇒ Object
-
.client_options(params, given = provided_keys(params)) ⇒ Object
Build the upload fields.
- .description ⇒ Object
- .details ⇒ Object
- .example_code ⇒ Object
- .is_supported?(platform) ⇒ Boolean
-
.landing_page_slug(params, given = provided_keys(params)) ⇒ Object
community_tokenis the legacy name forlanding_page_slug. -
.metadata_fields(metadata) ⇒ Object
Mobile App Distribution folds every
metadata_<key>field into the build's metadata, so one hash entry becomes one field. - .metrics_to_client(metrics) ⇒ Object
- .options_to_client(options) ⇒ Object
- .output ⇒ Object
-
.provided?(params, key, given = provided_keys(params)) ⇒ Boolean
True when the lane passed the param and gave it a real value.
-
.provided_keys(params) ⇒ Object
Keys the caller supplied, by lane argument or by env var.
- .run(params) ⇒ Object
-
.sync_to_saucelabs(params, given = provided_keys(params)) ⇒ Object
upload_to_saucelabsis the legacy field name; Mobile App Distribution readssync_to_saucelabs. - .upload_build(upload_url, ipa, options, timeout) ⇒ Object
-
.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.
- .warn_ignored_params(params, given = provided_keys(params)) ⇒ Object
Class Method Details
.authors ⇒ Object
480 481 482 |
# File 'lib/fastlane/plugin/saucelabs_appdist/actions/saucelabs_appdist_action.rb', line 480 def self. ["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. [ # 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
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.(params, given = provided_keys(params)) = {} params.values.each_key do |key| value = params[key] case key when *LOCAL_ONLY_PARAMS, *RECONCILED_PARAMS next when *PASSTHROUGH_PARAMS [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. [key] = value.to_s unless value.to_s.empty? when :testers_groups, :tags [key] = Array(value).join(',') when :metrics [key] = metrics_to_client(value).join(',') when :options [key] = (value).join(',') when :auto_update ['auto-update'] = value else UI.user_error!("Unknown parameter: #{key}") end end slug = landing_page_slug(params, given) [:community_token] = slug [:landing_page_slug] = slug sync = sync_to_saucelabs(params, given) [:upload_to_saucelabs] = sync [:sync_to_saucelabs] = sync .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
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.() Array().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.
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) .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) = self.(params, given) return path if Helper.test? response = self.upload_build(params[:upload_url], path, , params[:timeout]) UI.user_error!((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, , 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 [:file] = Faraday::UploadIO.new(ipa, 'application/octet-stream') if ipa && File.exist?(ipa) symbols_file = .delete(:symbols_file) if symbols_file [:symbols_file] = Faraday::UploadIO.new(symbols_file, 'application/octet-stream') end begin connection.post do |req| req..timeout = timeout req.url("/api/upload/") req.body = 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.(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 |