fastlane-plugin-saucelabs_appdist 0.3.2 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dee4f0403f875e1dbd4992e580e9514848176b9d635991b23c57b7be9fbd7b8d
4
- data.tar.gz: 7533e47446b5aa2847c95c5b1c8bbd0bde64143ea2e1067bf30b8a06c7526304
3
+ metadata.gz: 69bb3f94b51fc03eba9edc8cbd584119f403ff7e65fb5cb81d7817c8e397ed2c
4
+ data.tar.gz: 1afe02dd17556b0973e1f039e885610c8e8554fcae1ab5a592ebbfc2cc5f987e
5
5
  SHA512:
6
- metadata.gz: 86b1bff8f762f6d83a1ca1e81d53e91449625ab5efd9ee5ff930a57e5382a2fca2411210256b71f92dbd6381982e5f1e74aeeb87cb25305fe1867f6622885ea0
7
- data.tar.gz: a111d54819ab04693be15b23342d243c0dbbf2cc8ded10c0e83882650bf65649d2bbb7f2b12407867409ded82ffab9a8f3eadfdca25a33c053e49d44386ba683
6
+ metadata.gz: ab43059f2b0c361acc749afd6eda3793dab3da115388ce8a4075755788f5fbf35f6db7e0cff4d092a36b8ff2e3386833d5045cce0b9abf57b7f3ec11e27c8515
7
+ data.tar.gz: c6abcf359b775938a2ece9b150e7b986467b9dfcc86adadbb51a4e17cced715b678f57dd924f46c6fe575a0826f78eecd101f72d5e03e5ac8643f81843cbebb4
data/README.md CHANGED
@@ -29,6 +29,23 @@ saucelabs_appdist(
29
29
  )
30
30
  ```
31
31
 
32
+ ### Uploading to the same app every time
33
+
34
+ Mobile App Distribution matches an upload to an existing app by its bundle id, within the teams you can write to. When the same bundle id lives on more than one of your teams, the upload cannot tell them apart and is refused with code `136`. Pass `team_id` to say which team owns the app:
35
+
36
+ ```ruby
37
+ saucelabs_appdist(
38
+ api_key: "your_api_key",
39
+ ipa: "./path/to/app.ipa",
40
+ team_id: 42,
41
+ landing_page_slug: "my-app-beta"
42
+ )
43
+ ```
44
+
45
+ Every upload then appends a build to the same app, and `/install/my-app-beta` always serves the latest one. Re-sending the same `landing_page_slug` on every upload is safe — an app may keep claiming its own slug.
46
+
47
+ `folder_name` takes part in the match as well: an upload finds an existing app only when the folder matches what that app already has, and an upload that sends no folder looks only among apps that have none. So send the same `folder_name` on every upload, or none at all — changing it mid-pipeline routes the next build to a different app.
48
+
32
49
  ## Parameters
33
50
 
34
51
  | Key | Description | Default |
@@ -38,21 +55,41 @@ saucelabs_appdist(
38
55
  | `apk` | Path to your APK file (Android) | — |
39
56
  | `symbols_file` | Symbols mapping file | — |
40
57
  | `upload_url` | API URL for App Distribution | `https://app.testfairy.com` |
41
- | `testers_groups` | Array of tester groups | `[]` |
42
- | `metrics` | Array of metrics to record | `[]` |
58
+ | `api_version` | Upload transport. `:v3` arrives in plugin 3.x | `:legacy` |
59
+ | `team_id` | Team that owns the app. Required when a bundle id exists on several of your teams | `""` |
43
60
  | `comment` | Additional release notes | `No comment provided` |
44
- | `auto_update` | Auto-upgrade users (`on`/`off`) | `off` |
45
- | `notify` | Send email to testers (`on`/`off`) | `off` |
46
- | `options` | Array of options | `[]` |
47
- | `custom` | Custom options string | `""` |
48
- | `timeout` | Request timeout in seconds | — |
61
+ | `notify` | Send email to testers (`on`/`off`/`1`/`0`) | `off` |
49
62
  | `tags` | Custom tags for builds | `[]` |
63
+ | `metadata` | Key-value pairs stored on the build, returned under `metadata` | `{}` |
50
64
  | `folder_name` | Dashboard folder name | `""` |
51
65
  | `landing_page_mode` | Landing page visibility (`open`/`closed`) | `open` |
52
- | `upload_to_saucelabs` | Upload to Sauce Labs (`on`/`off`) | `off` |
53
- | `platform` | Platform override | `""` |
54
- | `community_token` | Custom URL token for the landing page | `""` |
55
- | `app_description` | Description text to display on the landing page | `""` |
66
+ | `landing_page_slug` | Custom URL token for the landing page, served at `/install/<slug>` | `""` |
67
+ | `community_token` | Legacy name for `landing_page_slug` | `""` |
68
+ | `sync_to_saucelabs` | Also upload the file to Sauce Labs app storage (`on`/`off`) | `off` |
69
+ | `platform` | Platform of a generic upload. Ignored for `.ipa`, `.apk` and `.aab` | `""` |
70
+ | `timeout` | Request timeout in seconds | — |
71
+
72
+ ### Legacy TestFairy parameters
73
+
74
+ Mobile App Distribution ignores the parameters below. Private-cloud instances still running legacy TestFairy honor them, so the plugin keeps sending them and warns when you set one.
75
+
76
+ | Key | Description | Default |
77
+ |-----|-------------|---------|
78
+ | `testers_groups` | Tester groups to notify. App Distribution notifies every tester on the project | `[]` |
79
+ | `app_description` | Landing page description. Set it from the App Distribution dashboard instead | `""` |
80
+ | `metrics` | Session metrics recorded by the TestFairy SDK | `[]` |
81
+ | `options` | Session options for the TestFairy SDK | `[]` |
82
+ | `custom` | Custom options string | `""` |
83
+ | `auto_update` | Auto-upgrade users (`on`/`off`) | `off` |
84
+ | `upload_to_saucelabs` | Deprecated. Renamed to `sync_to_saucelabs` | `off` |
85
+
86
+ ## Upgrading from 0.3.x
87
+
88
+ Your Fastfile keeps working. Three things change:
89
+
90
+ 1. **`upload_to_saucelabs` is deprecated.** Mobile App Distribution reads the field as `sync_to_saucelabs`, so the old name uploaded nothing. Rename it; the plugin warns until you do and sends both field names in the meantime.
91
+ 2. **Parameters App Distribution ignores now warn** instead of returning `status: ok` as though they applied. See the table above.
92
+ 3. **A failed upload raises the API's own error.** `UI.user_error!` carries the `code` and `message`, and the full envelope lands in `lane_context`.
56
93
 
57
94
  ## Response
58
95
 
@@ -77,15 +114,48 @@ Example response:
77
114
  "app_url": "https://app.testfairy.com/download/.../getapp",
78
115
  "invite_testers_url": "https://app.testfairy.com/projects/1/builds/1/invite",
79
116
  "icon_url": "https://app.testfairy.com/icons/.../icon.png",
80
- "options": "video-quality=medium,screenshot-interval=1,session-length=60m,video,logcat,shake,cpu,memory,phone-signal,battery,wifi",
117
+ "options": "",
81
118
  "platform": "iOS",
82
119
  "tags": [],
83
- "metadata": [],
84
- "has_testfairy_sdk": true,
120
+ "metadata": {},
121
+ "has_testfairy_sdk": false,
85
122
  "symbols_download_url": null,
86
123
  "attachments": null,
87
- "landing_page_url": "https://app.testfairy.com/join/xxxxxx",
88
- "build_specific_landing_page_url": "https://app.testfairy.com/join/xxxxxx?id=1",
89
- "landing_page_mode": "closed"
124
+ "landing_page_url": "https://app.testfairy.com/install/xxxxxx",
125
+ "build_specific_landing_page_url": "https://app.testfairy.com/install/0123456789abcdef0123456789abcdef",
126
+ "landing_page_mode": "closed",
127
+ "community_token": "xxxxxx",
128
+ "app_description": ""
90
129
  }
91
130
  ```
131
+
132
+ ## Errors
133
+
134
+ A failed upload raises a `FastlaneError` naming the API's own code and message, for example:
135
+
136
+ ```
137
+ Sauce Labs App Distribution upload failed with code 136: Invalid team_id.
138
+ ```
139
+
140
+ The full error envelope stays in `lane_context` so a lane can decide whether to retry:
141
+
142
+ ```ruby
143
+ begin
144
+ saucelabs_appdist(api_key: "...", ipa: "./app.ipa")
145
+ rescue FastlaneCore::Interface::FastlaneError
146
+ error = lane_context[SharedValues::SAUCELABS_APPDIST_UPLOAD_ERROR]
147
+ UI.message("code #{error['code']}: #{error['message']}")
148
+ raise
149
+ end
150
+ ```
151
+
152
+ Codes you are most likely to meet:
153
+
154
+ | Code | Meaning |
155
+ |------|---------|
156
+ | `5` | Invalid API key |
157
+ | `121` | File type not accepted |
158
+ | `124` | `landing_page_slug` is malformed |
159
+ | `125` | `landing_page_slug` already belongs to another app |
160
+ | `135` | `landing_page_mode` is neither `open` nor `closed` |
161
+ | `136` | The bundle id exists on several of your teams — pass `team_id` |
@@ -2,9 +2,37 @@ module Fastlane
2
2
  module Actions
3
3
  module SharedValues
4
4
  SAUCELABS_APPDIST_UPLOAD_RESPONSE = :SAUCELABS_APPDIST_UPLOAD_RESPONSE
5
+ SAUCELABS_APPDIST_UPLOAD_ERROR = :SAUCELABS_APPDIST_UPLOAD_ERROR
5
6
  end
6
7
 
7
8
  class SaucelabsAppdistAction < Action
9
+ # Config that stays on this side of the wire.
10
+ LOCAL_ONLY_PARAMS = %i[upload_url timeout api_version].freeze
11
+
12
+ # Params whose lane name is also the upload field name.
13
+ PASSTHROUGH_PARAMS = %i[
14
+ api_key ipa apk symbols_file comment notify custom platform
15
+ folder_name landing_page_mode app_description
16
+ ].freeze
17
+
18
+ # Params resolved as a pair or expanded into several fields.
19
+ RECONCILED_PARAMS = %i[
20
+ community_token landing_page_slug upload_to_saucelabs sync_to_saucelabs metadata
21
+ ].freeze
22
+
23
+ # Mobile App Distribution ignores these. Legacy TestFairy servers still
24
+ # read them, so they stay on the wire and only warn.
25
+ IGNORED_BY_APPDIST = {
26
+ testers_groups: 'Mobile App Distribution notifies every tester on the project; per-group notify is not available yet.',
27
+ app_description: 'Set the landing page description from the dashboard.',
28
+ metrics: 'Session metrics came from the TestFairy SDK, which Mobile App Distribution does not ship.',
29
+ options: 'Session options came from the TestFairy SDK, which Mobile App Distribution does not ship.',
30
+ custom: 'No equivalent.',
31
+ auto_update: 'No equivalent.'
32
+ }.freeze
33
+
34
+ BINARY_EXTENSIONS = %w(.ipa .apk .aab).freeze
35
+
8
36
  def self.upload_build(upload_url, ipa, options, timeout)
9
37
  require 'faraday'
10
38
  require 'faraday_middleware'
@@ -41,102 +69,190 @@ module Fastlane
41
69
  def self.run(params)
42
70
  UI.success('Starting with ipa upload to Sauce Labs App Distribution...')
43
71
 
44
- metrics_to_client = lambda do |metrics|
45
- metrics.map do |metric|
46
- case metric
47
- when :cpu, :memory, :network, :gps, :battery, :mic, :wifi
48
- metric.to_s
49
- when :phone_signal
50
- 'phone-signal'
51
- else
52
- UI.user_error!("Unknown metric: #{metric}")
53
- end
54
- end
55
- end
72
+ path = params[:ipa] || params[:apk]
73
+ UI.user_error!("No ipa or apk were given") unless path
56
74
 
57
- options_to_client = lambda do |options|
58
- options.map do |option|
59
- case option.to_sym
60
- when :shake, :anonymous
61
- option.to_s
62
- when :video_only_wifi
63
- 'video-only-wifi'
64
- else
65
- UI.user_error!("Unknown option: #{option}")
66
- end
67
- end
68
- end
75
+ # One snapshot, taken before `params.values` back-fills defaults into
76
+ # the same hash it reads from.
77
+ given = provided_keys(params)
78
+ warn_ignored_params(params, given)
79
+ client_options = self.client_options(params, given)
80
+
81
+ return path if Helper.test?
82
+
83
+ response = self.upload_build(params[:upload_url], path, client_options, params[:timeout])
84
+ UI.user_error!(upload_error_message(response)) unless parse_response(response)
85
+
86
+ UI.success("Build successfully uploaded to Sauce Labs App Distribution.")
87
+ UI.success("Response:\n#{JSON.pretty_generate(Actions.lane_context[SharedValues::SAUCELABS_APPDIST_UPLOAD_RESPONSE])}")
88
+ end
89
+
90
+ # Build the upload fields. Every declared option is either mapped here or
91
+ # named in LOCAL_ONLY_PARAMS — an option added without a mapping fails
92
+ # loud instead of vanishing.
93
+ def self.client_options(params, given = provided_keys(params))
94
+ options = {}
95
+
96
+ params.values.each_key do |key|
97
+ value = params[key]
69
98
 
70
- # Rejecting key `upload_url` and `timeout` as we don't need it in options
71
- client_options = Hash[params.values.reject do |key, value|
72
- [:upload_url, :timeout].include?(key)
73
- end.map do |key, value|
74
99
  case key
75
- when :api_key
76
- [key, value]
77
- when :ipa
78
- [key, value]
79
- when :apk
80
- [key, value]
81
- when :symbols_file
82
- [key, value]
83
- when :testers_groups
84
- [key, value.join(',')]
100
+ when *LOCAL_ONLY_PARAMS, *RECONCILED_PARAMS
101
+ next
102
+ when *PASSTHROUGH_PARAMS
103
+ options[key] = value
104
+ when :team_id
105
+ # 0.3.2 sent no team_id field at all, so don't start sending an
106
+ # empty one: a server reading its presence as "filter by team"
107
+ # would reject an upload that used to work.
108
+ options[key] = value.to_s unless value.to_s.empty?
109
+ when :testers_groups, :tags
110
+ options[key] = Array(value).join(',')
85
111
  when :metrics
86
- [key, metrics_to_client.call(value).join(',')]
87
- when :comment
88
- [key, value]
89
- when :auto_update
90
- ['auto-update', value]
91
- when :notify
92
- [key, value]
112
+ options[key] = metrics_to_client(value).join(',')
93
113
  when :options
94
- [key, options_to_client.call(value).join(',')]
95
- when :custom
96
- [key, value]
97
- when :tags
98
- [key, value.join(',')]
99
- when :folder_name
100
- [key, value]
101
- when :landing_page_mode
102
- [key, value]
103
- when :upload_to_saucelabs
104
- [key, value]
105
- when :platform
106
- [key, value]
107
- when :community_token
108
- [key, value]
109
- when :app_description
110
- [key, value]
114
+ options[key] = options_to_client(value).join(',')
115
+ when :auto_update
116
+ options['auto-update'] = value
111
117
  else
112
118
  UI.user_error!("Unknown parameter: #{key}")
113
119
  end
114
- end]
120
+ end
121
+
122
+ slug = landing_page_slug(params, given)
123
+ options[:community_token] = slug
124
+ options[:landing_page_slug] = slug
125
+
126
+ sync = sync_to_saucelabs(params, given)
127
+ options[:upload_to_saucelabs] = sync
128
+ options[:sync_to_saucelabs] = sync
129
+
130
+ options.merge(metadata_fields(params[:metadata]))
131
+ end
132
+
133
+ # `upload_to_saucelabs` is the legacy field name; Mobile App Distribution
134
+ # reads `sync_to_saucelabs`. Both go out so one lane works on either.
135
+ def self.sync_to_saucelabs(params, given = provided_keys(params))
136
+ legacy = provided?(params, :upload_to_saucelabs, given) ? params[:upload_to_saucelabs] : nil
137
+ current = provided?(params, :sync_to_saucelabs, given) ? params[:sync_to_saucelabs] : nil
138
+
139
+ if legacy && current && legacy != current
140
+ UI.user_error!("`upload_to_saucelabs: '#{legacy}'` contradicts `sync_to_saucelabs: '#{current}'`. Keep `sync_to_saucelabs`.")
141
+ end
142
+
143
+ UI.important("`upload_to_saucelabs` is deprecated — rename it to `sync_to_saucelabs`.") if legacy
144
+
145
+ current || legacy || params[:sync_to_saucelabs]
146
+ end
147
+
148
+ # `community_token` is the legacy name for `landing_page_slug`.
149
+ def self.landing_page_slug(params, given = provided_keys(params))
150
+ legacy = provided?(params, :community_token, given) ? params[:community_token] : nil
151
+ current = provided?(params, :landing_page_slug, given) ? params[:landing_page_slug] : nil
152
+
153
+ if legacy && current && legacy != current
154
+ UI.user_error!("`community_token: '#{legacy}'` contradicts `landing_page_slug: '#{current}'`. Pass one of them.")
155
+ end
156
+
157
+ current || legacy || ''
158
+ end
159
+
160
+ # Mobile App Distribution folds every `metadata_<key>` field into the
161
+ # build's metadata, so one hash entry becomes one field.
162
+ def self.metadata_fields(metadata)
163
+ (metadata || {}).each_with_object({}) do |(key, value), fields|
164
+ name = key.to_s.strip
165
+ UI.user_error!("Metadata keys cannot be empty") if name.empty?
166
+
167
+ fields["metadata_#{name}"] = value.to_s
168
+ end
169
+ end
170
+
171
+ def self.warn_ignored_params(params, given = provided_keys(params))
172
+ IGNORED_BY_APPDIST.each do |key, alternative|
173
+ next unless provided?(params, key, given)
174
+
175
+ UI.important("Sauce Labs Mobile App Distribution ignores `#{key}`. #{alternative} Legacy TestFairy servers still honor it.")
176
+ end
115
177
 
178
+ return unless provided?(params, :platform, given) && binary_upload?(params)
179
+
180
+ 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.')
181
+ end
182
+
183
+ # Keys the caller supplied, by lane argument or by env var.
184
+ #
185
+ # Two channels, read separately on purpose. `_values` holds ONLY
186
+ # lane-passed values and must be read before `params.values` back-fills
187
+ # defaults into that same hash. Env values never enter `_values` at all —
188
+ # fastlane consults them later, inside `fetch` — so asking only `_values`
189
+ # makes every FL_SAUCELABS_APPDIST_* value read as absent.
190
+ def self.provided_keys(params)
191
+ available_options.select do |option|
192
+ params._values.key?(option.key) || !option.fetch_env_value.nil?
193
+ end.map(&:key)
194
+ end
195
+
196
+ # True when the lane passed the param and gave it a real value.
197
+ def self.provided?(params, key, given = provided_keys(params))
198
+ given.include?(key) && ![nil, '', [], {}].include?(params[key])
199
+ end
200
+
201
+ def self.binary_upload?(params)
116
202
  path = params[:ipa] || params[:apk]
117
- UI.user_error!("No ipa or apk were given") unless path
203
+ return false unless path
118
204
 
119
- return path if Helper.test?
205
+ BINARY_EXTENSIONS.include?(File.extname(path.to_s).downcase)
206
+ end
120
207
 
121
- response = self.upload_build(params[:upload_url], path, client_options, params[:timeout])
122
- if parse_response(response)
123
- UI.success("Build successfully uploaded to Sauce Labs App Distribution.")
124
- UI.success("Response:\n#{JSON.pretty_generate(Actions.lane_context[SharedValues::SAUCELABS_APPDIST_UPLOAD_RESPONSE])}")
208
+ def self.metrics_to_client(metrics)
209
+ Array(metrics).map do |metric|
210
+ case metric.to_sym
211
+ when :cpu, :memory, :network, :gps, :battery, :mic, :wifi
212
+ metric.to_s
213
+ when :phone_signal
214
+ 'phone-signal'
215
+ else
216
+ UI.user_error!("Unknown metric: #{metric}")
217
+ end
218
+ end
219
+ end
220
+
221
+ def self.options_to_client(options)
222
+ Array(options).map do |option|
223
+ case option.to_sym
224
+ when :shake, :anonymous
225
+ option.to_s
226
+ when :video_only_wifi
227
+ 'video-only-wifi'
228
+ else
229
+ UI.user_error!("Unknown option: #{option}")
230
+ end
231
+ end
232
+ end
233
+
234
+ # Surface the API's own code and message so a lane can act on them, and
235
+ # keep the whole envelope in lane_context for retry logic.
236
+ def self.upload_error_message(response)
237
+ body = response.body
238
+ Actions.lane_context[SharedValues::SAUCELABS_APPDIST_UPLOAD_ERROR] = body
239
+
240
+ if body.is_a?(Hash) && body['code']
241
+ "Sauce Labs App Distribution upload failed with code #{body['code']}: #{body['message'] || '(no message)'}"
125
242
  else
126
- UI.user_error!("Error when trying to upload ipa to Sauce Labs App Distribution")
243
+ "Sauce Labs App Distribution upload failed with HTTP #{response.status}: #{body.inspect}"
127
244
  end
128
245
  end
129
246
 
130
247
  def self.parse_response(response)
131
- if response.body && response.body.key?('status') && response.body['status'] == 'ok'
132
- Actions.lane_context[SharedValues::SAUCELABS_APPDIST_UPLOAD_RESPONSE] = response.body
248
+ return false unless response.body.is_a?(Hash) && response.body['status'] == 'ok'
133
249
 
134
- return true
135
- else
136
- UI.error("Error uploading to Sauce Labs App Distribution: #{response.body}")
250
+ Actions.lane_context[SharedValues::SAUCELABS_APPDIST_UPLOAD_RESPONSE] = response.body
251
+ # A retry that succeeds must not leave the earlier failure's envelope
252
+ # behind for a later reader to mistake for this upload's result.
253
+ Actions.lane_context.delete(SharedValues::SAUCELABS_APPDIST_UPLOAD_ERROR)
137
254
 
138
- return false
139
- end
255
+ true
140
256
  end
141
257
  private_class_method :parse_response
142
258
 
@@ -193,18 +309,39 @@ module Fastlane
193
309
  description: "API URL for Sauce Labs App Distribution",
194
310
  default_value: "https://app.testfairy.com",
195
311
  optional: true),
312
+ FastlaneCore::ConfigItem.new(key: :api_version,
313
+ optional: true,
314
+ type: Symbol,
315
+ env_name: "FL_SAUCELABS_APPDIST_API_VERSION",
316
+ description: "Upload transport. This release supports :legacy (the /api/upload endpoint) only; :v3 arrives in plugin 3.x",
317
+ default_value: :legacy,
318
+ verify_block: proc do |value|
319
+ if value.to_sym == :v3
320
+ UI.user_error!("`api_version: :v3` needs plugin 3.x — this is #{Fastlane::SaucelabsAppdist::VERSION}, which uploads over the legacy /api/upload endpoint")
321
+ end
322
+ UI.user_error!("Unknown api_version `#{value}`. This release supports :legacy only") unless value.to_sym == :legacy
323
+ end),
324
+ FastlaneCore::ConfigItem.new(key: :team_id,
325
+ optional: true,
326
+ env_name: "FL_SAUCELABS_APPDIST_TEAM_ID",
327
+ skip_type_validation: true,
328
+ 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",
329
+ default_value: '',
330
+ verify_block: proc do |value|
331
+ UI.user_error!("The team_id must be a number") unless value.to_s.empty? || value.to_s.match?(/\A\d+\z/)
332
+ end),
196
333
  FastlaneCore::ConfigItem.new(key: :testers_groups,
197
334
  optional: true,
198
335
  type: Array,
199
336
  short_option: '-g',
200
337
  env_name: "FL_SAUCELABS_APPDIST_TESTERS_GROUPS",
201
- description: "Array of tester groups to be notified",
338
+ description: "Array of tester groups to be notified. Legacy TestFairy only — Mobile App Distribution notifies every tester on the project",
202
339
  default_value: []),
203
340
  FastlaneCore::ConfigItem.new(key: :metrics,
204
341
  optional: true,
205
342
  type: Array,
206
343
  env_name: "FL_SAUCELABS_APPDIST_METRICS",
207
- description: "Array of metrics to record (cpu,memory,network,phone_signal,gps,battery,mic,wifi)",
344
+ description: "Array of metrics to record (cpu,memory,network,phone_signal,gps,battery,mic,wifi). Legacy TestFairy SDK only",
208
345
  default_value: []),
209
346
  FastlaneCore::ConfigItem.new(key: :comment,
210
347
  optional: true,
@@ -214,23 +351,26 @@ module Fastlane
214
351
  FastlaneCore::ConfigItem.new(key: :auto_update,
215
352
  optional: true,
216
353
  env_name: "FL_SAUCELABS_APPDIST_AUTO_UPDATE",
217
- description: "Allows an easy upgrade of all users to the current version. To enable set to 'on'",
354
+ description: "Allows an easy upgrade of all users to the current version. To enable set to 'on'. Legacy TestFairy only",
218
355
  default_value: 'off'),
219
356
  FastlaneCore::ConfigItem.new(key: :notify,
220
357
  optional: true,
221
358
  env_name: "FL_SAUCELABS_APPDIST_NOTIFY",
222
- description: "Send email to testers",
223
- default_value: 'off'),
359
+ description: "Send email to testers. Can be 'on', 'off', '1' or '0'",
360
+ default_value: 'off',
361
+ verify_block: proc do |value|
362
+ 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)
363
+ end),
224
364
  FastlaneCore::ConfigItem.new(key: :options,
225
365
  optional: true,
226
366
  type: Array,
227
367
  env_name: "FL_SAUCELABS_APPDIST_OPTIONS",
228
- description: "Array of options (shake,video_only_wifi,anonymous)",
368
+ description: "Array of options (shake,video_only_wifi,anonymous). Legacy TestFairy SDK only",
229
369
  default_value: []),
230
370
  FastlaneCore::ConfigItem.new(key: :custom,
231
371
  optional: true,
232
372
  env_name: "FL_SAUCELABS_APPDIST_CUSTOM",
233
- description: "Array of custom options. Contact support for more information",
373
+ description: "Array of custom options. Legacy TestFairy only — contact support for more information",
234
374
  default_value: ''),
235
375
  FastlaneCore::ConfigItem.new(key: :timeout,
236
376
  env_name: "FL_SAUCELABS_APPDIST_TIMEOUT",
@@ -243,6 +383,12 @@ module Fastlane
243
383
  description: "Custom tags that can be used to organize your builds",
244
384
  type: Array,
245
385
  default_value: []),
386
+ FastlaneCore::ConfigItem.new(key: :metadata,
387
+ optional: true,
388
+ type: Hash,
389
+ env_name: "FL_SAUCELABS_APPDIST_METADATA",
390
+ 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",
391
+ default_value: {}),
246
392
  FastlaneCore::ConfigItem.new(key: :folder_name,
247
393
  optional: true,
248
394
  env_name: "FL_SAUCELABS_APPDIST_FOLDER_NAME",
@@ -256,8 +402,17 @@ module Fastlane
256
402
  verify_block: proc do |value|
257
403
  UI.user_error!("The landing page mode can only be open or closed") unless %w(open closed).include?(value)
258
404
  end),
405
+ FastlaneCore::ConfigItem.new(key: :sync_to_saucelabs,
406
+ optional: true,
407
+ env_name: "FL_SAUCELABS_APPDIST_SYNC_TO_SAUCELABS",
408
+ description: "Also upload the file to Sauce Labs app storage. It can be 'on' or 'off'",
409
+ default_value: 'off',
410
+ verify_block: proc do |value|
411
+ UI.user_error!("The sync to Sauce Labs can only be on or off") unless %w(on off).include?(value)
412
+ end),
259
413
  FastlaneCore::ConfigItem.new(key: :upload_to_saucelabs,
260
414
  optional: true,
415
+ deprecated: "Renamed to `sync_to_saucelabs`, which is the field Mobile App Distribution reads",
261
416
  env_name: "FL_SAUCELABS_APPDIST_UPLOAD_TO_SAUCELABS",
262
417
  description: "Upload file directly to Sauce Labs. It can be 'on' or 'off'",
263
418
  default_value: 'off',
@@ -267,17 +422,22 @@ module Fastlane
267
422
  FastlaneCore::ConfigItem.new(key: :platform,
268
423
  optional: true,
269
424
  env_name: "FL_SAUCELABS_APPDIST_PLATFORM",
270
- description: "Use if upload build is not iOS or Android. Contact support for more information",
425
+ description: "Platform of a generic upload. Ignored for .ipa, .apk and .aab, whose platform comes from the binary",
426
+ default_value: ''),
427
+ FastlaneCore::ConfigItem.new(key: :landing_page_slug,
428
+ optional: true,
429
+ env_name: "FL_SAUCELABS_APPDIST_LANDING_PAGE_SLUG",
430
+ description: "Custom URL token for the landing page, served at /install/<slug>. 6-63 chars: letters, digits, dot, hyphen or underscore",
271
431
  default_value: ''),
272
432
  FastlaneCore::ConfigItem.new(key: :community_token,
273
433
  optional: true,
274
434
  env_name: "FL_SAUCELABS_APPDIST_COMMUNITY_TOKEN",
275
- description: "Custom URL token for the landing page",
435
+ description: "Legacy name for `landing_page_slug`",
276
436
  default_value: ''),
277
437
  FastlaneCore::ConfigItem.new(key: :app_description,
278
438
  optional: true,
279
439
  env_name: "FL_SAUCELABS_APPDIST_APP_DESCRIPTION",
280
- description: "Description text to display on the landing page",
440
+ description: "Description text for the landing page. Legacy TestFairy only — Mobile App Distribution never writes it",
281
441
  default_value: '')
282
442
  ]
283
443
  end
@@ -293,7 +453,16 @@ module Fastlane
293
453
  api_key: "...",
294
454
  apk: "../build/app/outputs/apk/qa/release/app-qa-release.apk",
295
455
  comment: "Build #{lane_context[SharedValues::BUILD_NUMBER]}",
296
- )'
456
+ )',
457
+ '# Same bundle id on several teams: name the team so every upload
458
+ # appends to the same app instead of forking a new one.
459
+ saucelabs_appdist(
460
+ api_key: "...",
461
+ ipa: "./ipa_file.ipa",
462
+ team_id: 42,
463
+ landing_page_slug: "my-app-beta",
464
+ metadata: { branch: "main", ci_build: "1234" },
465
+ )'
297
466
  ]
298
467
  end
299
468
 
@@ -303,7 +472,8 @@ module Fastlane
303
472
 
304
473
  def self.output
305
474
  [
306
- ['SAUCELABS_APPDIST_UPLOAD_RESPONSE', 'Full response from the upload API']
475
+ ['SAUCELABS_APPDIST_UPLOAD_RESPONSE', 'Full response from the upload API'],
476
+ ['SAUCELABS_APPDIST_UPLOAD_ERROR', 'Full error envelope (status, code, message) from a failed upload']
307
477
  ]
308
478
  end
309
479
 
@@ -1,5 +1,5 @@
1
1
  module Fastlane
2
2
  module SaucelabsAppdist
3
- VERSION = "0.3.2"
3
+ VERSION = "2.0.0"
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fastlane-plugin-saucelabs_appdist
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.2
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sauce Labs
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-03-04 00:00:00.000000000 Z
11
+ date: 2026-09-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday