fastlane-plugin-saucelabs_appdist 0.3.1 → 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:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 69bb3f94b51fc03eba9edc8cbd584119f403ff7e65fb5cb81d7817c8e397ed2c
|
|
4
|
+
data.tar.gz: 1afe02dd17556b0973e1f039e885610c8e8554fcae1ab5a592ebbfc2cc5f987e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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,19 +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
|
-
| `
|
|
42
|
-
| `
|
|
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
|
-
| `
|
|
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
|
-
| `
|
|
53
|
-
| `
|
|
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`.
|
|
54
93
|
|
|
55
94
|
## Response
|
|
56
95
|
|
|
@@ -75,15 +114,48 @@ Example response:
|
|
|
75
114
|
"app_url": "https://app.testfairy.com/download/.../getapp",
|
|
76
115
|
"invite_testers_url": "https://app.testfairy.com/projects/1/builds/1/invite",
|
|
77
116
|
"icon_url": "https://app.testfairy.com/icons/.../icon.png",
|
|
78
|
-
"options": "
|
|
117
|
+
"options": "",
|
|
79
118
|
"platform": "iOS",
|
|
80
119
|
"tags": [],
|
|
81
|
-
"metadata":
|
|
82
|
-
"has_testfairy_sdk":
|
|
120
|
+
"metadata": {},
|
|
121
|
+
"has_testfairy_sdk": false,
|
|
83
122
|
"symbols_download_url": null,
|
|
84
123
|
"attachments": null,
|
|
85
|
-
"landing_page_url": "https://app.testfairy.com/
|
|
86
|
-
"build_specific_landing_page_url": "https://app.testfairy.com/
|
|
87
|
-
"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": ""
|
|
88
129
|
}
|
|
89
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,98 +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
|
-
|
|
45
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
|
76
|
-
|
|
77
|
-
when
|
|
78
|
-
[key
|
|
79
|
-
when :
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
|
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
|
|
95
|
-
when :
|
|
96
|
-
[
|
|
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]
|
|
114
|
+
options[key] = options_to_client(value).join(',')
|
|
115
|
+
when :auto_update
|
|
116
|
+
options['auto-update'] = value
|
|
107
117
|
else
|
|
108
118
|
UI.user_error!("Unknown parameter: #{key}")
|
|
109
119
|
end
|
|
110
|
-
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
slug = landing_page_slug(params, given)
|
|
123
|
+
options[:community_token] = slug
|
|
124
|
+
options[:landing_page_slug] = slug
|
|
111
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
|
|
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)
|
|
112
202
|
path = params[:ipa] || params[:apk]
|
|
113
|
-
|
|
203
|
+
return false unless path
|
|
114
204
|
|
|
115
|
-
|
|
205
|
+
BINARY_EXTENSIONS.include?(File.extname(path.to_s).downcase)
|
|
206
|
+
end
|
|
116
207
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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)'}"
|
|
121
242
|
else
|
|
122
|
-
|
|
243
|
+
"Sauce Labs App Distribution upload failed with HTTP #{response.status}: #{body.inspect}"
|
|
123
244
|
end
|
|
124
245
|
end
|
|
125
246
|
|
|
126
247
|
def self.parse_response(response)
|
|
127
|
-
|
|
128
|
-
Actions.lane_context[SharedValues::SAUCELABS_APPDIST_UPLOAD_RESPONSE] = response.body
|
|
248
|
+
return false unless response.body.is_a?(Hash) && response.body['status'] == 'ok'
|
|
129
249
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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)
|
|
133
254
|
|
|
134
|
-
|
|
135
|
-
end
|
|
255
|
+
true
|
|
136
256
|
end
|
|
137
257
|
private_class_method :parse_response
|
|
138
258
|
|
|
@@ -189,18 +309,39 @@ module Fastlane
|
|
|
189
309
|
description: "API URL for Sauce Labs App Distribution",
|
|
190
310
|
default_value: "https://app.testfairy.com",
|
|
191
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),
|
|
192
333
|
FastlaneCore::ConfigItem.new(key: :testers_groups,
|
|
193
334
|
optional: true,
|
|
194
335
|
type: Array,
|
|
195
336
|
short_option: '-g',
|
|
196
337
|
env_name: "FL_SAUCELABS_APPDIST_TESTERS_GROUPS",
|
|
197
|
-
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",
|
|
198
339
|
default_value: []),
|
|
199
340
|
FastlaneCore::ConfigItem.new(key: :metrics,
|
|
200
341
|
optional: true,
|
|
201
342
|
type: Array,
|
|
202
343
|
env_name: "FL_SAUCELABS_APPDIST_METRICS",
|
|
203
|
-
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",
|
|
204
345
|
default_value: []),
|
|
205
346
|
FastlaneCore::ConfigItem.new(key: :comment,
|
|
206
347
|
optional: true,
|
|
@@ -210,23 +351,26 @@ module Fastlane
|
|
|
210
351
|
FastlaneCore::ConfigItem.new(key: :auto_update,
|
|
211
352
|
optional: true,
|
|
212
353
|
env_name: "FL_SAUCELABS_APPDIST_AUTO_UPDATE",
|
|
213
|
-
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",
|
|
214
355
|
default_value: 'off'),
|
|
215
356
|
FastlaneCore::ConfigItem.new(key: :notify,
|
|
216
357
|
optional: true,
|
|
217
358
|
env_name: "FL_SAUCELABS_APPDIST_NOTIFY",
|
|
218
|
-
description: "Send email to testers",
|
|
219
|
-
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),
|
|
220
364
|
FastlaneCore::ConfigItem.new(key: :options,
|
|
221
365
|
optional: true,
|
|
222
366
|
type: Array,
|
|
223
367
|
env_name: "FL_SAUCELABS_APPDIST_OPTIONS",
|
|
224
|
-
description: "Array of options (shake,video_only_wifi,anonymous)",
|
|
368
|
+
description: "Array of options (shake,video_only_wifi,anonymous). Legacy TestFairy SDK only",
|
|
225
369
|
default_value: []),
|
|
226
370
|
FastlaneCore::ConfigItem.new(key: :custom,
|
|
227
371
|
optional: true,
|
|
228
372
|
env_name: "FL_SAUCELABS_APPDIST_CUSTOM",
|
|
229
|
-
description: "Array of custom options.
|
|
373
|
+
description: "Array of custom options. Legacy TestFairy only — contact support for more information",
|
|
230
374
|
default_value: ''),
|
|
231
375
|
FastlaneCore::ConfigItem.new(key: :timeout,
|
|
232
376
|
env_name: "FL_SAUCELABS_APPDIST_TIMEOUT",
|
|
@@ -239,6 +383,12 @@ module Fastlane
|
|
|
239
383
|
description: "Custom tags that can be used to organize your builds",
|
|
240
384
|
type: Array,
|
|
241
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: {}),
|
|
242
392
|
FastlaneCore::ConfigItem.new(key: :folder_name,
|
|
243
393
|
optional: true,
|
|
244
394
|
env_name: "FL_SAUCELABS_APPDIST_FOLDER_NAME",
|
|
@@ -252,8 +402,17 @@ module Fastlane
|
|
|
252
402
|
verify_block: proc do |value|
|
|
253
403
|
UI.user_error!("The landing page mode can only be open or closed") unless %w(open closed).include?(value)
|
|
254
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),
|
|
255
413
|
FastlaneCore::ConfigItem.new(key: :upload_to_saucelabs,
|
|
256
414
|
optional: true,
|
|
415
|
+
deprecated: "Renamed to `sync_to_saucelabs`, which is the field Mobile App Distribution reads",
|
|
257
416
|
env_name: "FL_SAUCELABS_APPDIST_UPLOAD_TO_SAUCELABS",
|
|
258
417
|
description: "Upload file directly to Sauce Labs. It can be 'on' or 'off'",
|
|
259
418
|
default_value: 'off',
|
|
@@ -263,7 +422,22 @@ module Fastlane
|
|
|
263
422
|
FastlaneCore::ConfigItem.new(key: :platform,
|
|
264
423
|
optional: true,
|
|
265
424
|
env_name: "FL_SAUCELABS_APPDIST_PLATFORM",
|
|
266
|
-
description: "
|
|
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",
|
|
431
|
+
default_value: ''),
|
|
432
|
+
FastlaneCore::ConfigItem.new(key: :community_token,
|
|
433
|
+
optional: true,
|
|
434
|
+
env_name: "FL_SAUCELABS_APPDIST_COMMUNITY_TOKEN",
|
|
435
|
+
description: "Legacy name for `landing_page_slug`",
|
|
436
|
+
default_value: ''),
|
|
437
|
+
FastlaneCore::ConfigItem.new(key: :app_description,
|
|
438
|
+
optional: true,
|
|
439
|
+
env_name: "FL_SAUCELABS_APPDIST_APP_DESCRIPTION",
|
|
440
|
+
description: "Description text for the landing page. Legacy TestFairy only — Mobile App Distribution never writes it",
|
|
267
441
|
default_value: '')
|
|
268
442
|
]
|
|
269
443
|
end
|
|
@@ -279,7 +453,16 @@ module Fastlane
|
|
|
279
453
|
api_key: "...",
|
|
280
454
|
apk: "../build/app/outputs/apk/qa/release/app-qa-release.apk",
|
|
281
455
|
comment: "Build #{lane_context[SharedValues::BUILD_NUMBER]}",
|
|
282
|
-
)'
|
|
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
|
+
)'
|
|
283
466
|
]
|
|
284
467
|
end
|
|
285
468
|
|
|
@@ -289,7 +472,8 @@ module Fastlane
|
|
|
289
472
|
|
|
290
473
|
def self.output
|
|
291
474
|
[
|
|
292
|
-
['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']
|
|
293
477
|
]
|
|
294
478
|
end
|
|
295
479
|
|
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.
|
|
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-
|
|
11
|
+
date: 2026-09-16 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: faraday
|
|
@@ -157,7 +157,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
157
157
|
- !ruby/object:Gem::Version
|
|
158
158
|
version: '0'
|
|
159
159
|
requirements: []
|
|
160
|
-
rubygems_version: 3.
|
|
160
|
+
rubygems_version: 3.5.3
|
|
161
161
|
signing_key:
|
|
162
162
|
specification_version: 4
|
|
163
163
|
summary: Sauce Labs plugin for uploading builds to Mobile App Distribution
|