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:
|
|
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,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
|
-
| `
|
|
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
|
-
| `
|
|
54
|
-
| `
|
|
55
|
-
| `
|
|
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": "
|
|
117
|
+
"options": "",
|
|
81
118
|
"platform": "iOS",
|
|
82
119
|
"tags": [],
|
|
83
|
-
"metadata":
|
|
84
|
-
"has_testfairy_sdk":
|
|
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/
|
|
88
|
-
"build_specific_landing_page_url": "https://app.testfairy.com/
|
|
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
|
-
|
|
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]
|
|
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
|
-
|
|
203
|
+
return false unless path
|
|
118
204
|
|
|
119
|
-
|
|
205
|
+
BINARY_EXTENSIONS.include?(File.extname(path.to_s).downcase)
|
|
206
|
+
end
|
|
120
207
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
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.
|
|
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: "
|
|
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: "
|
|
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
|
|
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
|
|
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
|