fastlane-plugin-appcenter 1.8.0 → 1.11.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +8 -7
- data/lib/fastlane/plugin/appcenter/actions/appcenter_fetch_version_number.rb +35 -16
- data/lib/fastlane/plugin/appcenter/actions/appcenter_upload_action.rb +78 -14
- data/lib/fastlane/plugin/appcenter/helper/appcenter_helper.rb +625 -163
- data/lib/fastlane/plugin/appcenter/version.rb +1 -1
- metadata +22 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b110405fe9e84494ddb201d8b05738652c29493a2d86cf33a83f63e3843c650f
|
4
|
+
data.tar.gz: a6a582ac374e02c45a5285b6960d8f0da7a44bb7f4375c2d001d25a3aad71d21
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6e30d4329779b399e127a8f31b2828ce307c586ea06539970d3b1ed338db5a1c4222364f269aab763fdb97b18abfc73de67ea0f1928397a3b8dc4b300998afd4
|
7
|
+
data.tar.gz: 766537e348299e02cd0e71f21502884487e76e333941e291229167e4554193d0284faff27eae2f91da24fc9b6a39425bc999e489001cefbd8582485c3f996b78
|
data/README.md
CHANGED
@@ -31,7 +31,6 @@ To get started, first, [obtain an API token](https://appcenter.ms/settings/apito
|
|
31
31
|
appcenter_fetch_devices(
|
32
32
|
api_token: "<appcenter token>",
|
33
33
|
owner_name: "<appcenter account name of the owner of the app (username or organization URL name)>",
|
34
|
-
owner_type: "user", # Default is user - set to organization for appcenter organizations
|
35
34
|
app_name: "<appcenter app name>",
|
36
35
|
destinations: "*", # Default is 'Collaborators', use '*' for all distribution groups
|
37
36
|
devices_file: "devices.txt" # Default. If you customize, the extension must be .txt
|
@@ -53,7 +52,8 @@ appcenter_upload(
|
|
53
52
|
appcenter_fetch_version_number(
|
54
53
|
api_token: "<appcenter token>",
|
55
54
|
owner_name: "<appcenter account name of the owner of the app (username or organization URL name)>",
|
56
|
-
app_name: "<appcenter app name (as seen in app URL)>"
|
55
|
+
app_name: "<appcenter app name (as seen in app URL)>",
|
56
|
+
version: "a specific version to get the last release for" # optional, don't set this value to get the last upload of all versions
|
57
57
|
)
|
58
58
|
```
|
59
59
|
|
@@ -90,7 +90,6 @@ Here is the list of all existing parameters:
|
|
90
90
|
| Key & Env Var | Description |
|
91
91
|
|-----------------|--------------------|
|
92
92
|
| `api_token` <br/> `APPCENTER_API_TOKEN` | API Token for App Center |
|
93
|
-
| `owner_type` <br/> `APPCENTER_OWNER_TYPE` | Owner type, either 'user' or 'organization' (default: `user`) |
|
94
93
|
| `owner_name` <br/> `APPCENTER_OWNER_NAME` | Owner name, as found in the App's URL in App Center |
|
95
94
|
| `destinations` <br/> `APPCENTER_DISTRIBUTE_DESTINATIONS` | Comma separated list of distribution group names. Default is 'Collaborators', use '*' for all distribution groups |
|
96
95
|
| `devices_file` <br/> `FL_REGISTER_DEVICES_FILE` | File to save the devices list to. Same environment variable as _fastlane_'s `register_devices` action |
|
@@ -104,7 +103,7 @@ Here is the list of all existing parameters:
|
|
104
103
|
| `owner_name` <br/> `APPCENTER_OWNER_NAME` | Owner name as found in the App's URL in App Center |
|
105
104
|
| `app_name` <br/> `APPCENTER_APP_NAME` | App name as found in the App's URL in App Center. If there is no app with such name, you will be prompted to create one |
|
106
105
|
| `app_display_name` <br/> `APPCENTER_APP_DISPLAY_NAME` | App display name to use when creating a new app |
|
107
|
-
| `app_os` <br/> `APPCENTER_APP_OS` | App OS. Used for new app creation, if app 'app_name' was not found |
|
106
|
+
| `app_os` <br/> `APPCENTER_APP_OS` | App OS can be Android, iOS, macOS, Windows, Custom. Used for new app creation, if app 'app_name' was not found |
|
108
107
|
| `app_platform` <br/> `APPCENTER_APP_PLATFORM` | App Platform. Used for new app creation, if app 'app_name' was not found |
|
109
108
|
| `file` <br/> `APPCENTER_DISTRIBUTE_FILE` | File path to the release build to publish |
|
110
109
|
| `upload_build_only` <br/> `APPCENTER_DISTRIBUTE_UPLOAD_BUILD_ONLY` | Flag to upload only the build to App Center. Skips uploading symbols or mapping (default: `false`) |
|
@@ -116,13 +115,14 @@ Here is the list of all existing parameters:
|
|
116
115
|
| `destination_type` <br/> `APPCENTER_DISTRIBUTE_DESTINATION_TYPE` | Destination type of distribution destination. 'group' and 'store' are supported (default: `group`) |
|
117
116
|
| `mandatory_update` <br/> `APPCENTER_DISTRIBUTE_MANDATORY_UPDATE` | Require users to update to this release. Ignored if destination type is 'store' (default: `false`) |
|
118
117
|
| `notify_testers` <br/> `APPCENTER_DISTRIBUTE_NOTIFY_TESTERS` | Send email notification about release. Ignored if destination type is 'store' (default: `false`) |
|
119
|
-
| `release_notes` <br/> `APPCENTER_DISTRIBUTE_RELEASE_NOTES` | Release notes
|
118
|
+
| `release_notes` <br/> `APPCENTER_DISTRIBUTE_RELEASE_NOTES` | Release notes (default: `No changelog given`) |
|
120
119
|
| `should_clip` <br/> `APPCENTER_DISTRIBUTE_RELEASE_NOTES_CLIPPING` | Clip release notes if its length is more then 5000, true by default (default: `true`) |
|
121
120
|
| `release_notes_link` <br/> `APPCENTER_DISTRIBUTE_RELEASE_NOTES_LINK` | Additional release notes link |
|
122
121
|
| `build_number` <br/> `APPCENTER_DISTRIBUTE_BUILD_NUMBER` | The build number, required for macOS .pkg and .dmg builds, as well as Android ProGuard `mapping.txt` when using `upload_mapping_only` |
|
123
122
|
| `version` <br/> `APPCENTER_DISTRIBUTE_VERSION` | The build version, required for .pkg, .dmg, .zip and .msi builds, as well as Android ProGuard `mapping.txt` when using `upload_mapping_only` |
|
124
|
-
| `timeout` <br/> `APPCENTER_DISTRIBUTE_TIMEOUT` | Request timeout in seconds |
|
123
|
+
| `timeout` <br/> `APPCENTER_DISTRIBUTE_TIMEOUT` | Request timeout in seconds applied to individual HTTP requests. Some commands use multiple HTTP requests, large file uploads are also split in multiple HTTP requests |
|
125
124
|
| `dsa_signature` <br/> `APPCENTER_DISTRIBUTE_DSA_SIGNATURE` | DSA signature of the macOS or Windows release for Sparkle update feed |
|
125
|
+
| `ed_signature` <br/> `APPCENTER_DISTRIBUTE_ED_SIGNATURE` | EdDSA signature of the macOS or Windows release for Sparkle update feed |
|
126
126
|
| `strict` <br/> `APPCENTER_STRICT_MODE` | Strict mode, set to 'true' to fail early in case a potential error was detected |
|
127
127
|
|
128
128
|
#### `appcenter_fetch_version_number`
|
@@ -132,6 +132,7 @@ Here is the list of all existing parameters:
|
|
132
132
|
| `api_token` <br/> `APPCENTER_API_TOKEN` | API Token for App Center |
|
133
133
|
| `owner_name` <br/> `APPCENTER_OWNER_NAME` | Owner name, as found in the App's URL in App Center |
|
134
134
|
| `app_name` <br/> `APPCENTER_APP_NAME` | App name as found in the App's URL in App Center. If there is no app with such name, you will be prompted to create one |
|
135
|
+
| `version` <br/> `APPCENTER_APP_VERSION` | App version to get the last release for instead of the last release of all versions |
|
135
136
|
|
136
137
|
## Example
|
137
138
|
|
@@ -185,4 +186,4 @@ Check out [SECURITY.md](SECURITY.md) for any security concern with this project.
|
|
185
186
|
|
186
187
|
## Contact
|
187
188
|
|
188
|
-
We're on Twitter as [@vsappcenter](https://www.twitter.com/vsappcenter). Additionally you can reach out to us on the [App Center](https://appcenter.ms/apps) portal
|
189
|
+
We're on Twitter as [@vsappcenter](https://www.twitter.com/vsappcenter). Additionally you can reach out to us on the [App Center](https://appcenter.ms/apps) portal. Open the "?" menu on the top right corner of screen, then use "Contact support" to file a support ticket. Our support team is there to answer your questions and help you solve your problems.
|
@@ -1,6 +1,6 @@
|
|
1
|
-
require
|
2
|
-
require
|
3
|
-
require
|
1
|
+
require "json"
|
2
|
+
require "net/http"
|
3
|
+
require "fastlane_core/ui/ui"
|
4
4
|
|
5
5
|
module Fastlane
|
6
6
|
UI = FastlaneCore::UI unless Fastlane.const_defined?("UI")
|
@@ -8,38 +8,51 @@ module Fastlane
|
|
8
8
|
module Actions
|
9
9
|
class AppcenterFetchVersionNumberAction < Action
|
10
10
|
def self.description
|
11
|
-
"Fetches the latest version number of an app from App Center"
|
11
|
+
"Fetches the latest version number of an app or the last build number of a version from App Center"
|
12
12
|
end
|
13
13
|
|
14
14
|
def self.authors
|
15
|
-
["jspargo", "ShopKeep"]
|
15
|
+
["jspargo", "ShopKeep", "Qutaibah"]
|
16
16
|
end
|
17
17
|
|
18
18
|
def self.run(params)
|
19
19
|
api_token = params[:api_token]
|
20
20
|
app_name = params[:app_name]
|
21
21
|
owner_name = params[:owner_name]
|
22
|
+
version = params[:version]
|
22
23
|
|
23
24
|
releases = Helper::AppcenterHelper.fetch_releases(
|
24
25
|
api_token: api_token,
|
25
26
|
owner_name: owner_name,
|
26
|
-
app_name: app_name
|
27
|
+
app_name: app_name,
|
27
28
|
)
|
28
29
|
|
29
30
|
UI.abort_with_message!("No versions found for '#{app_name}' owned by #{owner_name}") unless releases
|
30
|
-
|
31
|
-
|
31
|
+
|
32
|
+
sorted_releases = releases
|
33
|
+
|
34
|
+
if version.nil?
|
35
|
+
sorted_releases = releases.sort_by { |release| release["id"] }
|
36
|
+
else
|
37
|
+
sorted_releases = releases.select { |release| release["short_version"] == version }.sort_by { |release| release["id"] }
|
38
|
+
end
|
39
|
+
|
40
|
+
latest_release = sorted_releases.last
|
32
41
|
|
33
42
|
if latest_release.nil?
|
34
|
-
|
43
|
+
if version.nil?
|
44
|
+
UI.user_error!("This app has no releases yet")
|
45
|
+
return nil
|
46
|
+
end
|
47
|
+
UI.user_error!("The provided version (#{version}) has no releases yet")
|
35
48
|
return nil
|
36
49
|
end
|
37
50
|
|
38
51
|
return {
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
52
|
+
"id" => latest_release["id"],
|
53
|
+
"version" => latest_release["short_version"],
|
54
|
+
"build_number" => latest_release["version"],
|
55
|
+
}
|
43
56
|
end
|
44
57
|
|
45
58
|
def self.available_options
|
@@ -61,7 +74,13 @@ module Fastlane
|
|
61
74
|
description: "Name of the application on App Center",
|
62
75
|
verify_block: proc do |value|
|
63
76
|
UI.user_error!("No app name for App Center given, pass using `app_name: 'app name'`") unless value && !value.empty?
|
64
|
-
end)
|
77
|
+
end),
|
78
|
+
FastlaneCore::ConfigItem.new(key: :version,
|
79
|
+
env_name: "APPCENTER_APP_VERSION",
|
80
|
+
description: "The version to get the latest release for",
|
81
|
+
optional: true,
|
82
|
+
type: String),
|
83
|
+
|
65
84
|
]
|
66
85
|
end
|
67
86
|
|
@@ -70,11 +89,11 @@ module Fastlane
|
|
70
89
|
end
|
71
90
|
|
72
91
|
def self.get_apps(api_token)
|
73
|
-
host_uri = URI.parse(
|
92
|
+
host_uri = URI.parse("https://api.appcenter.ms")
|
74
93
|
http = Net::HTTP.new(host_uri.host, host_uri.port)
|
75
94
|
http.use_ssl = true
|
76
95
|
apps_request = Net::HTTP::Get.new("/v0.1/apps")
|
77
|
-
apps_request[
|
96
|
+
apps_request["X-API-Token"] = api_token
|
78
97
|
apps_response = http.request(apps_request)
|
79
98
|
return [] unless apps_response.kind_of?(Net::HTTPOK)
|
80
99
|
return JSON.parse(apps_response.body)
|
@@ -9,6 +9,23 @@ module Fastlane
|
|
9
9
|
windows: %w(.appx .appxbundle .appxupload .msix .msixbundle .msixupload .zip .msi),
|
10
10
|
custom: %w(.zip)
|
11
11
|
}
|
12
|
+
CONTENT_TYPES = {
|
13
|
+
apk: "application/vnd.android.package-archive",
|
14
|
+
aab: "application/vnd.android.package-archive",
|
15
|
+
msi: "application/x-msi",
|
16
|
+
plist: "application/xml",
|
17
|
+
aetx: "application/c-x509-ca-cert",
|
18
|
+
cer: "application/pkix-cert",
|
19
|
+
xap: "application/x-silverlight-app",
|
20
|
+
appx: "application/x-appx",
|
21
|
+
appxbundle: "application/x-appxbundle",
|
22
|
+
appxupload: "application/x-appxupload",
|
23
|
+
appxsym: "application/x-appxupload",
|
24
|
+
msix: "application/x-msix",
|
25
|
+
msixbundle: "application/x-msixbundle",
|
26
|
+
msixupload: "application/x-msixupload",
|
27
|
+
msixsym: "application/x-msixupload",
|
28
|
+
}
|
12
29
|
ALL_SUPPORTED_EXTENSIONS = SUPPORTED_EXTENSIONS.values.flatten.sort!.uniq!
|
13
30
|
STORE_ONLY_EXTENSIONS = %w(.aab)
|
14
31
|
STORE_SUPPORTED_EXTENSIONS = %w(.aab .apk .ipa)
|
@@ -126,6 +143,7 @@ module Fastlane
|
|
126
143
|
build_number = params[:build_number]
|
127
144
|
version = params[:version]
|
128
145
|
dsa_signature = params[:dsa_signature]
|
146
|
+
ed_signature = params[:ed_signature]
|
129
147
|
|
130
148
|
if release_notes.length >= Constants::MAX_RELEASE_NOTES_LENGTH
|
131
149
|
unless should_clip
|
@@ -180,25 +198,40 @@ module Fastlane
|
|
180
198
|
File.delete zip_file
|
181
199
|
end
|
182
200
|
UI.message("Creating zip archive: #{zip_file}")
|
183
|
-
file = Actions::ZipAction.run(path: file, output_path: zip_file)
|
201
|
+
file = Actions::ZipAction.run(path: file, output_path: zip_file, symlinks: true)
|
184
202
|
end
|
185
203
|
|
186
204
|
UI.message("Starting release upload...")
|
187
205
|
upload_details = Helper::AppcenterHelper.create_release_upload(api_token, owner_name, app_name, release_upload_body)
|
188
206
|
if upload_details
|
189
|
-
upload_id = upload_details['
|
190
|
-
|
207
|
+
upload_id = upload_details['id']
|
208
|
+
|
209
|
+
UI.message("Setting Metadata...")
|
210
|
+
content_type = Constants::CONTENT_TYPES[File.extname(file)&.delete('.').downcase.to_sym] || "application/octet-stream"
|
211
|
+
set_metadata_url = "#{upload_details['upload_domain']}/upload/set_metadata/#{upload_details['package_asset_id']}?file_name=#{File.basename(file)}&file_size=#{File.size(file)}&token=#{upload_details['url_encoded_token']}&content_type=#{content_type}"
|
212
|
+
chunk_size = Helper::AppcenterHelper.set_release_upload_metadata(set_metadata_url, api_token, owner_name, app_name, upload_id, timeout)
|
213
|
+
UI.abort_with_message!("Upload aborted") unless chunk_size
|
191
214
|
|
192
215
|
UI.message("Uploading release binary...")
|
193
|
-
|
216
|
+
upload_url = "#{upload_details['upload_domain']}/upload/upload_chunk/#{upload_details['package_asset_id']}?token=#{upload_details['url_encoded_token']}"
|
217
|
+
uploaded = Helper::AppcenterHelper.upload_build(api_token, owner_name, app_name, file, upload_id, upload_url, content_type, chunk_size, timeout)
|
218
|
+
UI.abort_with_message!("Upload aborted") unless uploaded
|
219
|
+
|
220
|
+
UI.message("Finishing release...")
|
221
|
+
finish_url = "#{upload_details['upload_domain']}/upload/finished/#{upload_details['package_asset_id']}?token=#{upload_details['url_encoded_token']}"
|
222
|
+
finished = Helper::AppcenterHelper.finish_release_upload(finish_url, api_token, owner_name, app_name, upload_id, timeout)
|
223
|
+
UI.abort_with_message!("Upload aborted") unless finished
|
224
|
+
|
225
|
+
UI.message("Waiting for release to be ready...")
|
226
|
+
release_status_url = "v0.1/apps/#{owner_name}/#{app_name}/uploads/releases/#{upload_id}"
|
227
|
+
release_id = Helper::AppcenterHelper.poll_for_release_id(api_token, release_status_url)
|
194
228
|
|
195
|
-
if
|
196
|
-
release_id = uploaded['release_id']
|
229
|
+
if release_id.is_a? Integer
|
197
230
|
release_url = Helper::AppcenterHelper.get_release_url(owner_type, owner_name, app_name, release_id)
|
198
231
|
UI.message("Release '#{release_id}' committed: #{release_url}")
|
199
232
|
|
200
233
|
release = Helper::AppcenterHelper.update_release(api_token, owner_name, app_name, release_id, release_notes)
|
201
|
-
Helper::AppcenterHelper.update_release_metadata(api_token, owner_name, app_name, release_id, dsa_signature)
|
234
|
+
Helper::AppcenterHelper.update_release_metadata(api_token, owner_name, app_name, release_id, dsa_signature, ed_signature)
|
202
235
|
|
203
236
|
destinations_array = []
|
204
237
|
if destinations == '*'
|
@@ -252,10 +285,11 @@ module Fastlane
|
|
252
285
|
app_platform = params[:app_platform]
|
253
286
|
|
254
287
|
platforms = {
|
255
|
-
Android: %w[Java React-Native Xamarin],
|
256
|
-
iOS: %w[Objective-C-Swift React-Native Xamarin],
|
288
|
+
Android: %w[Java React-Native Xamarin Unity],
|
289
|
+
iOS: %w[Objective-C-Swift React-Native Xamarin Unity],
|
257
290
|
macOS: %w[Objective-C-Swift],
|
258
|
-
Windows: %w[UWP WPF WinForms Unity]
|
291
|
+
Windows: %w[UWP WPF WinForms Unity],
|
292
|
+
Custom: %w[Custom]
|
259
293
|
}
|
260
294
|
|
261
295
|
begin
|
@@ -284,6 +318,30 @@ module Fastlane
|
|
284
318
|
end
|
285
319
|
end
|
286
320
|
|
321
|
+
def self.add_app_to_distribution_group_if_needed(params)
|
322
|
+
return unless params[:destination_type] == 'group' && params[:owner_type] == 'organization' && params[:destinations] != '*'
|
323
|
+
|
324
|
+
app_distribution_groups = Helper::AppcenterHelper.fetch_distribution_groups(
|
325
|
+
api_token: params[:api_token],
|
326
|
+
owner_name: params[:owner_name],
|
327
|
+
app_name: params[:app_name]
|
328
|
+
)
|
329
|
+
|
330
|
+
group_names = app_distribution_groups.map { |g| g['name'] }
|
331
|
+
destination_names = params[:destinations].split(',').map(&:strip)
|
332
|
+
|
333
|
+
destination_names.each do |destination_name|
|
334
|
+
unless group_names.include? destination_name
|
335
|
+
Helper::AppcenterHelper.add_new_app_to_distribution_group(
|
336
|
+
api_token: params[:api_token],
|
337
|
+
owner_name: params[:owner_name],
|
338
|
+
app_name: params[:app_name],
|
339
|
+
destination_name: destination_name
|
340
|
+
)
|
341
|
+
end
|
342
|
+
end
|
343
|
+
end
|
344
|
+
|
287
345
|
def self.run(params)
|
288
346
|
values = params.values
|
289
347
|
upload_build_only = params[:upload_build_only]
|
@@ -294,10 +352,10 @@ module Fastlane
|
|
294
352
|
|
295
353
|
# if app found or successfully created
|
296
354
|
if self.get_or_create_app(params)
|
355
|
+
self.add_app_to_distribution_group_if_needed(params)
|
297
356
|
release = self.run_release_upload(params) unless upload_dsym_only || upload_mapping_only
|
298
357
|
params[:version] = release['short_version'] if release
|
299
358
|
params[:build_number] = release['version'] if release
|
300
|
-
|
301
359
|
self.run_dsym_upload(params) unless upload_mapping_only || upload_build_only
|
302
360
|
self.run_mapping_upload(params) unless upload_dsym_only || upload_build_only
|
303
361
|
end
|
@@ -368,7 +426,7 @@ module Fastlane
|
|
368
426
|
|
369
427
|
FastlaneCore::ConfigItem.new(key: :app_os,
|
370
428
|
env_name: "APPCENTER_APP_OS",
|
371
|
-
description: "App OS. Used for new app creation, if app 'app_name' was not found",
|
429
|
+
description: "App OS can be Android, iOS, macOS, Windows, Custom. Used for new app creation, if app 'app_name' was not found",
|
372
430
|
optional: true,
|
373
431
|
type: String),
|
374
432
|
|
@@ -577,7 +635,7 @@ module Fastlane
|
|
577
635
|
|
578
636
|
FastlaneCore::ConfigItem.new(key: :timeout,
|
579
637
|
env_name: "APPCENTER_DISTRIBUTE_TIMEOUT",
|
580
|
-
description: "Request timeout in seconds",
|
638
|
+
description: "Request timeout in seconds applied to individual HTTP requests. Some commands use multiple HTTP requests, large file uploads are also split in multiple HTTP requests",
|
581
639
|
optional: true,
|
582
640
|
type: Integer),
|
583
641
|
|
@@ -586,7 +644,13 @@ module Fastlane
|
|
586
644
|
description: "DSA signature of the macOS or Windows release for Sparkle update feed",
|
587
645
|
optional: true,
|
588
646
|
type: String),
|
589
|
-
|
647
|
+
|
648
|
+
FastlaneCore::ConfigItem.new(key: :ed_signature,
|
649
|
+
env_name: "APPCENTER_DISTRIBUTE_ED_SIGNATURE",
|
650
|
+
description: "EdDSA signature of the macOS or Windows release for Sparkle update feed",
|
651
|
+
optional: true,
|
652
|
+
type: String),
|
653
|
+
|
590
654
|
FastlaneCore::ConfigItem.new(key: :strict,
|
591
655
|
env_name: "APPCENTER_STRICT_MODE",
|
592
656
|
description: "Strict mode, set to 'true' to fail early in case a potential error was detected",
|
@@ -1,7 +1,22 @@
|
|
1
|
+
class File
|
2
|
+
def each_chunk(chunk_size)
|
3
|
+
yield read(chunk_size) until eof?
|
4
|
+
end
|
5
|
+
end
|
6
|
+
|
1
7
|
module Fastlane
|
2
8
|
module Helper
|
3
9
|
class AppcenterHelper
|
4
10
|
|
11
|
+
# Time to wait between 2 status polls in seconds
|
12
|
+
RELEASE_UPLOAD_STATUS_POLL_INTERVAL = 1
|
13
|
+
|
14
|
+
# Maximum number of retries for a request
|
15
|
+
MAX_REQUEST_RETRIES = 2
|
16
|
+
|
17
|
+
# Delay between retries in seconds
|
18
|
+
REQUEST_RETRY_INTERVAL = 5
|
19
|
+
|
5
20
|
# basic utility method to check file types that App Center will accept,
|
6
21
|
# accounting for file types that can and should be zip-compressed
|
7
22
|
# before they are uploaded
|
@@ -18,10 +33,16 @@ module Fastlane
|
|
18
33
|
require 'faraday'
|
19
34
|
require 'faraday_middleware'
|
20
35
|
|
36
|
+
default_api_url = "https://api.appcenter.ms"
|
37
|
+
if ENV['APPCENTER_ENV']&.upcase == 'INT'
|
38
|
+
default_api_url = "https://api-gateway-core-integration.dev.avalanch.es"
|
39
|
+
end
|
21
40
|
options = {
|
22
|
-
url: upload_url ||
|
41
|
+
url: upload_url || default_api_url
|
23
42
|
}
|
24
43
|
|
44
|
+
UI.message("DEBUG: BASE URL #{options[:url]}") if ENV['DEBUG']
|
45
|
+
|
25
46
|
Faraday.new(options) do |builder|
|
26
47
|
if upload_url
|
27
48
|
builder.request :multipart unless dsym
|
@@ -41,16 +62,29 @@ module Fastlane
|
|
41
62
|
# upload_url
|
42
63
|
def self.create_release_upload(api_token, owner_name, app_name, body)
|
43
64
|
connection = self.connection
|
65
|
+
url = "v0.1/apps/#{owner_name}/#{app_name}/uploads/releases"
|
66
|
+
body ||= {}
|
67
|
+
|
68
|
+
UI.message("DEBUG: POST #{url}") if ENV['DEBUG']
|
69
|
+
UI.message("DEBUG: POST body: #{JSON.pretty_generate(body)}\n") if ENV['DEBUG']
|
44
70
|
|
45
|
-
response =
|
46
|
-
|
47
|
-
|
48
|
-
|
71
|
+
status, message, response = retry_429_and_error do
|
72
|
+
response = connection.post(url) do |req|
|
73
|
+
req.headers['X-API-Token'] = api_token
|
74
|
+
req.headers['internal-request-source'] = "fastlane"
|
75
|
+
req.body = body
|
76
|
+
end
|
49
77
|
end
|
50
78
|
|
51
|
-
case
|
79
|
+
case status
|
80
|
+
when 0, 429
|
81
|
+
if status == 0
|
82
|
+
UI.error("Faraday http exception creating release upload: #{message}")
|
83
|
+
else
|
84
|
+
UI.error("Retryable error creating release upload #{status}: #{message}")
|
85
|
+
end
|
86
|
+
false
|
52
87
|
when 200...300
|
53
|
-
UI.message("DEBUG: #{JSON.pretty_generate(response.body)}\n") if ENV['DEBUG']
|
54
88
|
response.body
|
55
89
|
when 401
|
56
90
|
UI.user_error!("Auth Error, provided invalid token")
|
@@ -59,7 +93,7 @@ module Fastlane
|
|
59
93
|
UI.error("Not found, invalid owner or application name")
|
60
94
|
false
|
61
95
|
when 500...600
|
62
|
-
UI.
|
96
|
+
UI.abort_with_message!("Internal Service Error, please try again later")
|
63
97
|
else
|
64
98
|
UI.error("Error #{response.status}: #{response.body}")
|
65
99
|
false
|
@@ -74,20 +108,34 @@ module Fastlane
|
|
74
108
|
def self.create_mapping_upload(api_token, owner_name, app_name, file_name, build_number, version)
|
75
109
|
connection = self.connection
|
76
110
|
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
111
|
+
url = "v0.1/apps/#{owner_name}/#{app_name}/symbol_uploads"
|
112
|
+
body = {
|
113
|
+
symbol_type: "AndroidProguard",
|
114
|
+
file_name: file_name,
|
115
|
+
build: build_number,
|
116
|
+
version: version,
|
117
|
+
}
|
118
|
+
|
119
|
+
UI.message("DEBUG: POST #{url}") if ENV['DEBUG']
|
120
|
+
UI.message("DEBUG: POST body #{JSON.pretty_generate(body)}\n") if ENV['DEBUG']
|
121
|
+
|
122
|
+
status, message, response = retry_429_and_error do
|
123
|
+
response = connection.post(url) do |req|
|
124
|
+
req.headers['X-API-Token'] = api_token
|
125
|
+
req.headers['internal-request-source'] = "fastlane"
|
126
|
+
req.body = body
|
127
|
+
end
|
86
128
|
end
|
87
129
|
|
88
|
-
case
|
130
|
+
case status
|
131
|
+
when 0, 429
|
132
|
+
if status == 0
|
133
|
+
UI.error("Faraday http exception creating mapping upload: #{message}")
|
134
|
+
else
|
135
|
+
UI.error("Retryable error creating mapping upload #{status}: #{message}")
|
136
|
+
end
|
137
|
+
false
|
89
138
|
when 200...300
|
90
|
-
UI.message("DEBUG: #{JSON.pretty_generate(response.body)}\n") if ENV['DEBUG']
|
91
139
|
response.body
|
92
140
|
when 401
|
93
141
|
UI.user_error!("Auth Error, provided invalid token")
|
@@ -109,17 +157,31 @@ module Fastlane
|
|
109
157
|
def self.create_dsym_upload(api_token, owner_name, app_name)
|
110
158
|
connection = self.connection
|
111
159
|
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
160
|
+
url = "v0.1/apps/#{owner_name}/#{app_name}/symbol_uploads"
|
161
|
+
body = {
|
162
|
+
symbol_type: 'Apple'
|
163
|
+
}
|
164
|
+
|
165
|
+
UI.message("DEBUG: POST #{url}") if ENV['DEBUG']
|
166
|
+
UI.message("DEBUG: POST body #{JSON.pretty_generate(body)}\n") if ENV['DEBUG']
|
167
|
+
|
168
|
+
status, message, response = retry_429_and_error do
|
169
|
+
response = connection.post(url) do |req|
|
170
|
+
req.headers['X-API-Token'] = api_token
|
171
|
+
req.headers['internal-request-source'] = "fastlane"
|
172
|
+
req.body = body
|
173
|
+
end
|
118
174
|
end
|
119
175
|
|
120
|
-
case
|
176
|
+
case status
|
177
|
+
when 0, 429
|
178
|
+
if status == 0
|
179
|
+
UI.error("Faraday http exception creating dsym upload: #{message}")
|
180
|
+
else
|
181
|
+
UI.error("Retryable error creating dsym upload #{status}: #{message}")
|
182
|
+
end
|
183
|
+
false
|
121
184
|
when 200...300
|
122
|
-
UI.message("DEBUG: #{JSON.pretty_generate(response.body)}\n") if ENV['DEBUG']
|
123
185
|
response.body
|
124
186
|
when 401
|
125
187
|
UI.user_error!("Auth Error, provided invalid token")
|
@@ -137,17 +199,31 @@ module Fastlane
|
|
137
199
|
def self.update_symbol_upload(api_token, owner_name, app_name, symbol_upload_id, status)
|
138
200
|
connection = self.connection
|
139
201
|
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
202
|
+
url = "v0.1/apps/#{owner_name}/#{app_name}/symbol_uploads/#{symbol_upload_id}"
|
203
|
+
body = {
|
204
|
+
status: status
|
205
|
+
}
|
206
|
+
|
207
|
+
UI.message("DEBUG: PATCH #{url}") if ENV['DEBUG']
|
208
|
+
UI.message("DEBUG: PATCH body #{JSON.pretty_generate(body)}\n") if ENV['DEBUG']
|
147
209
|
|
148
|
-
|
210
|
+
status, message, response = retry_429_and_error do
|
211
|
+
response = connection.patch(url) do |req|
|
212
|
+
req.headers['X-API-Token'] = api_token
|
213
|
+
req.headers['internal-request-source'] = "fastlane"
|
214
|
+
req.body = body
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
case status
|
219
|
+
when 0, 429
|
220
|
+
if status == 0
|
221
|
+
UI.error("Faraday http exception updating symbol upload: #{message}")
|
222
|
+
else
|
223
|
+
UI.error("Retryable error updating symbol upload #{status}: #{message}")
|
224
|
+
end
|
225
|
+
false
|
149
226
|
when 200...300
|
150
|
-
UI.message("DEBUG: #{JSON.pretty_generate(response.body)}\n") if ENV['DEBUG']
|
151
227
|
response.body
|
152
228
|
when 401
|
153
229
|
UI.user_error!("Auth Error, provided invalid token")
|
@@ -164,84 +240,225 @@ module Fastlane
|
|
164
240
|
def self.upload_symbol(api_token, owner_name, app_name, symbol, symbol_type, symbol_upload_id, upload_url)
|
165
241
|
connection = self.connection(upload_url, true)
|
166
242
|
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
end
|
243
|
+
UI.message("DEBUG: PUT #{upload_url}") if ENV['DEBUG']
|
244
|
+
UI.message("DEBUG: PUT body <data>\n") if ENV['DEBUG']
|
245
|
+
|
246
|
+
log_type = "dSYM" if symbol_type == "Apple"
|
247
|
+
log_type = "mapping" if symbol_type == "Android"
|
173
248
|
|
174
|
-
|
175
|
-
|
249
|
+
status, message, response = retry_429_and_error do
|
250
|
+
response = connection.put do |req|
|
251
|
+
req.headers['x-ms-blob-type'] = "BlockBlob"
|
252
|
+
req.headers['Content-Length'] = File.size(symbol).to_s
|
253
|
+
req.headers['internal-request-source'] = "fastlane"
|
254
|
+
req.body = Faraday::UploadIO.new(symbol, 'application/octet-stream') if symbol && File.exist?(symbol)
|
255
|
+
end
|
256
|
+
end
|
176
257
|
|
177
|
-
case
|
258
|
+
case status
|
259
|
+
when 0, 429
|
260
|
+
if status == 0
|
261
|
+
UI.error("Faraday http exception updating symbol upload: #{message}")
|
262
|
+
else
|
263
|
+
UI.error("Retryable error updating symbol upload #{status}: #{message}")
|
264
|
+
end
|
265
|
+
false
|
178
266
|
when 200...300
|
179
267
|
self.update_symbol_upload(api_token, owner_name, app_name, symbol_upload_id, 'committed')
|
180
|
-
UI.success("#{
|
268
|
+
UI.success("#{log_type} uploaded")
|
181
269
|
when 401
|
182
270
|
UI.user_error!("Auth Error, provided invalid token")
|
183
271
|
false
|
184
272
|
else
|
185
|
-
UI.error("Error uploading #{
|
273
|
+
UI.error("Error uploading #{log_type} #{response.status}: #{response.body}")
|
186
274
|
self.update_symbol_upload(api_token, owner_name, app_name, symbol_upload_id, 'aborted')
|
187
|
-
UI.error("#{
|
275
|
+
UI.error("#{log_type} upload aborted")
|
188
276
|
false
|
189
277
|
end
|
190
278
|
end
|
191
279
|
|
192
|
-
#
|
193
|
-
#
|
194
|
-
#
|
195
|
-
def self.
|
196
|
-
connection = self.connection(
|
280
|
+
# sets metadata for new upload in App Center
|
281
|
+
# returns:
|
282
|
+
# chunk size
|
283
|
+
def self.set_release_upload_metadata(set_metadata_url, api_token, owner_name, app_name, upload_id, timeout)
|
284
|
+
connection = self.connection(set_metadata_url)
|
285
|
+
|
286
|
+
UI.message("DEBUG: POST #{set_metadata_url}") if ENV['DEBUG']
|
287
|
+
UI.message("DEBUG: POST body <data>\n") if ENV['DEBUG']
|
197
288
|
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
289
|
+
status, message, response = retry_429_and_error do
|
290
|
+
response = connection.post do |req|
|
291
|
+
req.options.timeout = timeout
|
292
|
+
req.headers['internal-request-source'] = "fastlane"
|
293
|
+
end
|
294
|
+
end
|
295
|
+
|
296
|
+
case status
|
297
|
+
when 0, 429
|
298
|
+
if status == 0
|
299
|
+
UI.error("Faraday http exception releasing upload metadata: #{message}")
|
300
|
+
else
|
301
|
+
UI.error("Retryable error releasing upload metadata #{status}: #{message}")
|
302
|
+
end
|
303
|
+
false
|
304
|
+
when 200...300
|
305
|
+
chunk_size = response.body['chunk_size']
|
306
|
+
unless chunk_size.is_a? Integer
|
307
|
+
UI.error("Set metadata didn't return chunk size: #{response.status}: #{response.body}")
|
308
|
+
false
|
309
|
+
else
|
310
|
+
UI.message("Metadata set")
|
311
|
+
chunk_size
|
312
|
+
end
|
313
|
+
when 401
|
314
|
+
UI.user_error!("Auth Error, provided invalid token")
|
315
|
+
false
|
316
|
+
else
|
317
|
+
UI.error("Error setting metadata: #{response.status}: #{response.body}")
|
318
|
+
false
|
319
|
+
end
|
320
|
+
end
|
202
321
|
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
322
|
+
# Verifies a successful upload to App Center
|
323
|
+
# returns:
|
324
|
+
# successful upload response body.
|
325
|
+
def self.finish_release_upload(finish_url, api_token, owner_name, app_name, upload_id, timeout)
|
326
|
+
connection = self.connection(finish_url)
|
327
|
+
|
328
|
+
UI.message("DEBUG: POST #{finish_url}") if ENV['DEBUG']
|
329
|
+
|
330
|
+
status, message, response = retry_429_and_error do
|
331
|
+
response = connection.post do |req|
|
332
|
+
req.options.timeout = timeout
|
333
|
+
req.headers['internal-request-source'] = "fastlane"
|
334
|
+
end
|
207
335
|
end
|
208
336
|
|
209
|
-
case
|
337
|
+
case status
|
338
|
+
when 0, 429
|
339
|
+
if status == 0
|
340
|
+
UI.error("Faraday http exception finishing release upload: #{message}")
|
341
|
+
else
|
342
|
+
UI.error("Retryable error finishing release upload #{status}: #{message}")
|
343
|
+
end
|
344
|
+
false
|
210
345
|
when 200...300
|
211
|
-
|
212
|
-
|
346
|
+
if response.body['error'] == false
|
347
|
+
UI.message("Upload finished")
|
348
|
+
self.update_release_upload(api_token, owner_name, app_name, upload_id, 'uploadFinished')
|
349
|
+
else
|
350
|
+
UI.error("Error finishing upload: #{response.body['message']}")
|
351
|
+
false
|
352
|
+
end
|
213
353
|
when 401
|
214
354
|
UI.user_error!("Auth Error, provided invalid token")
|
215
355
|
false
|
216
356
|
else
|
217
|
-
UI.error("Error
|
218
|
-
self.update_release_upload(api_token, owner_name, app_name, upload_id, 'aborted')
|
219
|
-
UI.error("Release aborted")
|
357
|
+
UI.error("Error finishing upload: #{response.status}: #{response.body}")
|
220
358
|
false
|
221
359
|
end
|
222
360
|
end
|
223
361
|
|
362
|
+
# upload binary for specified upload_url
|
363
|
+
# if succeed, then commits the release
|
364
|
+
# otherwise aborts
|
365
|
+
def self.upload_build(api_token, owner_name, app_name, file, upload_id, upload_url, content_type, chunk_size, timeout)
|
366
|
+
block_number = 1
|
367
|
+
|
368
|
+
File.open(file).each_chunk(chunk_size) do |chunk|
|
369
|
+
upload_chunk_url = "#{upload_url}&block_number=#{block_number}"
|
370
|
+
retries = 0
|
371
|
+
|
372
|
+
while retries <= MAX_REQUEST_RETRIES
|
373
|
+
begin
|
374
|
+
connection = self.connection(upload_chunk_url, true)
|
375
|
+
|
376
|
+
UI.message("DEBUG: POST #{upload_chunk_url}") if ENV['DEBUG']
|
377
|
+
UI.message("DEBUG: POST body <data>\n") if ENV['DEBUG']
|
378
|
+
response = connection.post do |req|
|
379
|
+
req.options.timeout = timeout
|
380
|
+
req.headers['internal-request-source'] = "fastlane"
|
381
|
+
req.headers['Content-Length'] = chunk.length.to_s
|
382
|
+
req.headers['Content-Type'] = 'application/octet-stream'
|
383
|
+
req.body = chunk
|
384
|
+
end
|
385
|
+
UI.message("DEBUG: #{response.status} #{JSON.pretty_generate(response.body)}\n") if ENV['DEBUG']
|
386
|
+
status = response.status
|
387
|
+
message = response.body
|
388
|
+
rescue Faraday::Error => e
|
389
|
+
|
390
|
+
# Low level HTTP errors, we will retry them
|
391
|
+
status = 0
|
392
|
+
message = e.message
|
393
|
+
end
|
394
|
+
|
395
|
+
case status
|
396
|
+
when 200...300
|
397
|
+
if response.body['error'] == false
|
398
|
+
UI.message("Chunk uploaded")
|
399
|
+
block_number += 1
|
400
|
+
break
|
401
|
+
else
|
402
|
+
UI.error("Error uploading binary #{response.body['message']}")
|
403
|
+
return false
|
404
|
+
end
|
405
|
+
when 401
|
406
|
+
UI.user_error!("Auth Error, provided invalid token")
|
407
|
+
return false
|
408
|
+
when 400...407, 409...428, 430...499
|
409
|
+
UI.user_error!("Client error: #{response.status}: #{response.body}")
|
410
|
+
return false
|
411
|
+
else
|
412
|
+
if retries < MAX_REQUEST_RETRIES
|
413
|
+
UI.message("DEBUG: Retryable error uploading binary #{status}: #{message}")
|
414
|
+
retries += 1
|
415
|
+
sleep(REQUEST_RETRY_INTERVAL)
|
416
|
+
else
|
417
|
+
UI.error("Error uploading binary #{status}: #{message}")
|
418
|
+
return false
|
419
|
+
end
|
420
|
+
end
|
421
|
+
end
|
422
|
+
end
|
423
|
+
UI.message("Binary uploaded")
|
424
|
+
end
|
425
|
+
|
224
426
|
# Commits or aborts the upload process for a release
|
225
427
|
def self.update_release_upload(api_token, owner_name, app_name, upload_id, status)
|
226
428
|
connection = self.connection
|
227
429
|
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
430
|
+
url = "v0.1/apps/#{owner_name}/#{app_name}/uploads/releases/#{upload_id}"
|
431
|
+
body = {
|
432
|
+
upload_status: status,
|
433
|
+
id: upload_id
|
434
|
+
}
|
435
|
+
|
436
|
+
UI.message("DEBUG: PATCH #{url}") if ENV['DEBUG']
|
437
|
+
UI.message("DEBUG: PATCH body #{JSON.pretty_generate(body)}\n") if ENV['DEBUG']
|
438
|
+
|
439
|
+
status, message, response = retry_429_and_error do
|
440
|
+
response = connection.patch(url) do |req|
|
441
|
+
req.headers['X-API-Token'] = api_token
|
442
|
+
req.headers['internal-request-source'] = "fastlane"
|
443
|
+
req.body = body
|
444
|
+
end
|
234
445
|
end
|
235
446
|
|
236
|
-
case
|
447
|
+
case status
|
448
|
+
when 0, 429
|
449
|
+
if status == 0
|
450
|
+
UI.error("Faraday http exception updating release upload: #{message}")
|
451
|
+
else
|
452
|
+
UI.error("Retryable error updating release upload #{status}: #{message}")
|
453
|
+
end
|
454
|
+
false
|
237
455
|
when 200...300
|
238
|
-
UI.message("DEBUG: #{JSON.pretty_generate(response.body)}\n") if ENV['DEBUG']
|
239
456
|
response.body
|
240
457
|
when 401
|
241
458
|
UI.user_error!("Auth Error, provided invalid token")
|
242
459
|
false
|
243
460
|
when 500...600
|
244
|
-
UI.
|
461
|
+
UI.abort_with_message!("Internal Service Error, please try again later")
|
245
462
|
else
|
246
463
|
UI.error("Error #{response.status}: #{response.body}")
|
247
464
|
false
|
@@ -251,15 +468,28 @@ module Fastlane
|
|
251
468
|
# get existing release
|
252
469
|
def self.get_release(api_token, owner_name, app_name, release_id)
|
253
470
|
connection = self.connection
|
254
|
-
|
255
|
-
|
256
|
-
|
471
|
+
|
472
|
+
url = "v0.1/apps/#{owner_name}/#{app_name}/releases/#{release_id}"
|
473
|
+
|
474
|
+
UI.message("DEBUG: GET #{url}") if ENV['DEBUG']
|
475
|
+
|
476
|
+
status, message, response = retry_429_and_error do
|
477
|
+
response = connection.get(url) do |req|
|
478
|
+
req.headers['X-API-Token'] = api_token
|
479
|
+
req.headers['internal-request-source'] = "fastlane"
|
480
|
+
end
|
257
481
|
end
|
258
482
|
|
259
|
-
case
|
483
|
+
case status
|
484
|
+
when 0, 429
|
485
|
+
if status == 0
|
486
|
+
UI.error("Faraday http exception getting release: #{message}")
|
487
|
+
else
|
488
|
+
UI.error("Retryable error getting release: #{status}: #{message}")
|
489
|
+
end
|
490
|
+
false
|
260
491
|
when 200...300
|
261
492
|
release = response.body
|
262
|
-
UI.message("DEBUG: #{JSON.pretty_generate(release)}") if ENV['DEBUG']
|
263
493
|
release
|
264
494
|
when 404
|
265
495
|
UI.error("Not found, invalid release url")
|
@@ -273,19 +503,73 @@ module Fastlane
|
|
273
503
|
end
|
274
504
|
end
|
275
505
|
|
506
|
+
# Polls the upload for a release id. When a release is uploaded, we have to check
|
507
|
+
# for a successful extraction before we can continue.
|
508
|
+
# returns:
|
509
|
+
# release_distinct_id
|
510
|
+
def self.poll_for_release_id(api_token, url)
|
511
|
+
connection = self.connection
|
512
|
+
|
513
|
+
while true
|
514
|
+
UI.message("DEBUG: GET #{url}") if ENV['DEBUG']
|
515
|
+
|
516
|
+
status, message, response = retry_429_and_error do
|
517
|
+
response = connection.get(url) do |req|
|
518
|
+
req.headers['X-API-Token'] = api_token
|
519
|
+
req.headers['internal-request-source'] = "fastlane"
|
520
|
+
end
|
521
|
+
end
|
522
|
+
|
523
|
+
case status
|
524
|
+
when 0, 429
|
525
|
+
if status == 0
|
526
|
+
UI.error("Faraday http exception polling for release id: #{message}")
|
527
|
+
else
|
528
|
+
UI.error("Retryable error polling for release id: #{status}: #{message}")
|
529
|
+
end
|
530
|
+
return false
|
531
|
+
when 200...300
|
532
|
+
case response.body['upload_status']
|
533
|
+
when "readyToBePublished"
|
534
|
+
return response.body['release_distinct_id']
|
535
|
+
when "error"
|
536
|
+
UI.error("Error fetching release: #{response.body['error_details']}")
|
537
|
+
return false
|
538
|
+
else
|
539
|
+
sleep(RELEASE_UPLOAD_STATUS_POLL_INTERVAL)
|
540
|
+
end
|
541
|
+
else
|
542
|
+
UI.error("Error fetching information about release #{response.status}: #{response.body}")
|
543
|
+
return false
|
544
|
+
end
|
545
|
+
end
|
546
|
+
end
|
547
|
+
|
276
548
|
# get distribution group or store
|
277
549
|
def self.get_destination(api_token, owner_name, app_name, destination_type, destination_name)
|
278
550
|
connection = self.connection
|
279
551
|
|
280
|
-
|
281
|
-
|
282
|
-
|
552
|
+
url = "v0.1/apps/#{owner_name}/#{app_name}/distribution_#{destination_type}s/#{ERB::Util.url_encode(destination_name)}"
|
553
|
+
|
554
|
+
UI.message("DEBUG: GET #{url}") if ENV['DEBUG']
|
555
|
+
|
556
|
+
status, message, response = retry_429_and_error do
|
557
|
+
response = connection.get(url) do |req|
|
558
|
+
req.headers['X-API-Token'] = api_token
|
559
|
+
req.headers['internal-request-source'] = "fastlane"
|
560
|
+
end
|
283
561
|
end
|
284
562
|
|
285
|
-
case
|
563
|
+
case status
|
564
|
+
when 0, 429
|
565
|
+
if status == 0
|
566
|
+
UI.error("Faraday http exception getting destination: #{message}")
|
567
|
+
else
|
568
|
+
UI.error("Retryable error getting destination: #{status}: #{message}")
|
569
|
+
end
|
570
|
+
false
|
286
571
|
when 200...300
|
287
572
|
destination = response.body
|
288
|
-
UI.message("DEBUG: received #{destination_type} #{JSON.pretty_generate(destination)}") if ENV['DEBUG']
|
289
573
|
destination
|
290
574
|
when 404
|
291
575
|
UI.error("Not found, invalid distribution #{destination_type} name")
|
@@ -303,15 +587,30 @@ module Fastlane
|
|
303
587
|
def self.update_release(api_token, owner_name, app_name, release_id, release_notes = '')
|
304
588
|
connection = self.connection
|
305
589
|
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
590
|
+
url = "v0.1/apps/#{owner_name}/#{app_name}/releases/#{release_id}"
|
591
|
+
body = {
|
592
|
+
release_notes: release_notes
|
593
|
+
}
|
594
|
+
|
595
|
+
UI.message("DEBUG: PUT #{url}") if ENV['DEBUG']
|
596
|
+
UI.message("DEBUG: PUT body #{JSON.pretty_generate(body)}\n") if ENV['DEBUG']
|
597
|
+
|
598
|
+
status, message, response = retry_429_and_error do
|
599
|
+
response = connection.put(url) do |req|
|
600
|
+
req.headers['X-API-Token'] = api_token
|
601
|
+
req.headers['internal-request-source'] = "fastlane"
|
602
|
+
req.body = body
|
603
|
+
end
|
312
604
|
end
|
313
605
|
|
314
|
-
case
|
606
|
+
case status
|
607
|
+
when 0, 429
|
608
|
+
if status == 0
|
609
|
+
UI.error("Faraday http exception updating release: #{message}")
|
610
|
+
else
|
611
|
+
UI.error("Retryable error updating release: #{status}: #{message}")
|
612
|
+
end
|
613
|
+
false
|
315
614
|
when 200...300
|
316
615
|
# get full release info
|
317
616
|
release = self.get_release(api_token, owner_name, app_name, release_id)
|
@@ -319,8 +618,6 @@ module Fastlane
|
|
319
618
|
|
320
619
|
download_url = release['download_url']
|
321
620
|
|
322
|
-
UI.message("DEBUG: #{JSON.pretty_generate(release)}") if ENV['DEBUG']
|
323
|
-
|
324
621
|
Actions.lane_context[Fastlane::Actions::SharedValues::APPCENTER_DOWNLOAD_LINK] = download_url
|
325
622
|
Actions.lane_context[Fastlane::Actions::SharedValues::APPCENTER_BUILD_INFORMATION] = release
|
326
623
|
|
@@ -340,23 +637,42 @@ module Fastlane
|
|
340
637
|
end
|
341
638
|
|
342
639
|
# updates release metadata
|
343
|
-
def self.update_release_metadata(api_token, owner_name, app_name, release_id, dsa_signature)
|
344
|
-
return if dsa_signature.to_s == ''
|
640
|
+
def self.update_release_metadata(api_token, owner_name, app_name, release_id, dsa_signature = '', ed_signature = '')
|
641
|
+
return if dsa_signature.to_s == '' && ed_signature.to_s == ''
|
345
642
|
|
346
|
-
|
347
|
-
|
643
|
+
url = "v0.1/apps/#{owner_name}/#{app_name}/releases/#{release_id}"
|
644
|
+
body = {
|
645
|
+
metadata: {}
|
348
646
|
}
|
647
|
+
|
648
|
+
if dsa_signature.to_s != ''
|
649
|
+
body[:metadata]["dsa_signature"] = dsa_signature
|
650
|
+
end
|
651
|
+
if ed_signature.to_s != ''
|
652
|
+
body[:metadata]["ed_signature"] = ed_signature
|
653
|
+
end
|
654
|
+
|
655
|
+
UI.message("DEBUG: PATCH #{url}") if ENV['DEBUG']
|
656
|
+
UI.message("DEBUG: PATCH body #{JSON.pretty_generate(body)}\n") if ENV['DEBUG']
|
349
657
|
|
350
658
|
connection = self.connection
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
659
|
+
|
660
|
+
status, message, response = retry_429_and_error do
|
661
|
+
response = connection.patch(url) do |req|
|
662
|
+
req.headers['X-API-Token'] = api_token
|
663
|
+
req.headers['internal-request-source'] = "fastlane"
|
664
|
+
req.body = body
|
665
|
+
end
|
357
666
|
end
|
358
667
|
|
359
|
-
case
|
668
|
+
case status
|
669
|
+
when 0, 429
|
670
|
+
if status == 0
|
671
|
+
UI.error("Faraday http exception updating release metadata: #{message}")
|
672
|
+
else
|
673
|
+
UI.error("Retryable error updating release metadata: #{status}: #{message}")
|
674
|
+
end
|
675
|
+
false
|
360
676
|
when 200...300
|
361
677
|
UI.message("Release Metadata was successfully updated for release '#{release_id}'")
|
362
678
|
when 404
|
@@ -375,21 +691,35 @@ module Fastlane
|
|
375
691
|
def self.add_to_destination(api_token, owner_name, app_name, release_id, destination_type, destination_id, mandatory_update = false, notify_testers = false)
|
376
692
|
connection = self.connection
|
377
693
|
|
378
|
-
|
694
|
+
url = "v0.1/apps/#{owner_name}/#{app_name}/releases/#{release_id}/#{destination_type}s"
|
695
|
+
body = {
|
696
|
+
id: destination_id
|
697
|
+
}
|
379
698
|
|
380
|
-
body = { "id" => destination_id }
|
381
699
|
if destination_type == "group"
|
382
700
|
body["mandatory_update"] = mandatory_update
|
383
701
|
body["notify_testers"] = notify_testers
|
384
702
|
end
|
385
703
|
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
704
|
+
UI.message("DEBUG: POST #{url}") if ENV['DEBUG']
|
705
|
+
UI.message("DEBUG: POST body #{JSON.pretty_generate(body)}\n") if ENV['DEBUG']
|
706
|
+
|
707
|
+
status, message, response = retry_429_and_error do
|
708
|
+
response = connection.post(url) do |req|
|
709
|
+
req.headers['X-API-Token'] = api_token
|
710
|
+
req.headers['internal-request-source'] = "fastlane"
|
711
|
+
req.body = body
|
712
|
+
end
|
390
713
|
end
|
391
714
|
|
392
|
-
case
|
715
|
+
case status
|
716
|
+
when 0, 429
|
717
|
+
if status == 0
|
718
|
+
UI.error("Faraday http exception adding to destination: #{message}")
|
719
|
+
else
|
720
|
+
UI.error("Retryable error adding to destination: #{status}: #{message}")
|
721
|
+
end
|
722
|
+
false
|
393
723
|
when 200...300
|
394
724
|
# get full release info
|
395
725
|
release = self.get_release(api_token, owner_name, app_name, release_id)
|
@@ -397,8 +727,6 @@ module Fastlane
|
|
397
727
|
|
398
728
|
download_url = release['download_url']
|
399
729
|
|
400
|
-
UI.message("DEBUG: received release #{JSON.pretty_generate(release)}") if ENV['DEBUG']
|
401
|
-
|
402
730
|
Actions.lane_context[Fastlane::Actions::SharedValues::APPCENTER_DOWNLOAD_LINK] = download_url
|
403
731
|
Actions.lane_context[Fastlane::Actions::SharedValues::APPCENTER_BUILD_INFORMATION] = release
|
404
732
|
|
@@ -421,12 +749,25 @@ module Fastlane
|
|
421
749
|
def self.get_app(api_token, owner_name, app_name)
|
422
750
|
connection = self.connection
|
423
751
|
|
424
|
-
|
425
|
-
|
426
|
-
|
752
|
+
url = "v0.1/apps/#{owner_name}/#{app_name}"
|
753
|
+
|
754
|
+
UI.message("DEBUG: GET #{url}") if ENV['DEBUG']
|
755
|
+
|
756
|
+
status, message, response = retry_429_and_error do
|
757
|
+
response = connection.get(url) do |req|
|
758
|
+
req.headers['X-API-Token'] = api_token
|
759
|
+
req.headers['internal-request-source'] = "fastlane"
|
760
|
+
end
|
427
761
|
end
|
428
762
|
|
429
|
-
case
|
763
|
+
case status
|
764
|
+
when 0, 429
|
765
|
+
if status == 0
|
766
|
+
UI.error("Faraday http exception getting app: #{message}")
|
767
|
+
else
|
768
|
+
UI.error("Retryable error getting app: #{status}: #{message}")
|
769
|
+
end
|
770
|
+
false
|
430
771
|
when 200...300
|
431
772
|
UI.message("DEBUG: #{JSON.pretty_generate(response.body)}\n") if ENV['DEBUG']
|
432
773
|
true
|
@@ -446,23 +787,35 @@ module Fastlane
|
|
446
787
|
def self.create_app(api_token, owner_type, owner_name, app_name, app_display_name, os, platform)
|
447
788
|
connection = self.connection
|
448
789
|
|
449
|
-
|
790
|
+
url = owner_type == "user" ? "v0.1/apps" : "v0.1/orgs/#{owner_name}/apps"
|
791
|
+
body = {
|
792
|
+
display_name: app_display_name,
|
793
|
+
name: app_name,
|
794
|
+
os: os,
|
795
|
+
platform: platform
|
796
|
+
}
|
797
|
+
|
798
|
+
UI.message("DEBUG: POST #{url}") if ENV['DEBUG']
|
799
|
+
UI.message("DEBUG: POST body #{JSON.pretty_generate(body)}\n") if ENV['DEBUG']
|
450
800
|
|
451
|
-
response =
|
452
|
-
|
453
|
-
|
454
|
-
|
455
|
-
|
456
|
-
|
457
|
-
"os" => os,
|
458
|
-
"platform" => platform
|
459
|
-
}
|
801
|
+
status, message, response = retry_429_and_error do
|
802
|
+
response = connection.post(url) do |req|
|
803
|
+
req.headers['X-API-Token'] = api_token
|
804
|
+
req.headers['internal-request-source'] = "fastlane"
|
805
|
+
req.body = body
|
806
|
+
end
|
460
807
|
end
|
461
808
|
|
462
|
-
case
|
809
|
+
case status
|
810
|
+
when 0, 429
|
811
|
+
if status == 0
|
812
|
+
UI.error("Faraday http exception creating app: #{message}")
|
813
|
+
else
|
814
|
+
UI.error("Retryable error creating app: #{status}: #{message}")
|
815
|
+
end
|
816
|
+
false
|
463
817
|
when 200...300
|
464
818
|
created = response.body
|
465
|
-
UI.message("DEBUG: #{JSON.pretty_generate(created)}") if ENV['DEBUG']
|
466
819
|
UI.success("Created #{os}/#{platform} app with name \"#{created['name']}\" and display name \"#{created['display_name']}\" for #{owner_type} \"#{owner_name}\"")
|
467
820
|
true
|
468
821
|
when 401
|
@@ -477,16 +830,26 @@ module Fastlane
|
|
477
830
|
def self.fetch_distribution_groups(api_token:, owner_name:, app_name:)
|
478
831
|
connection = self.connection
|
479
832
|
|
480
|
-
|
833
|
+
url = "/v0.1/apps/#{owner_name}/#{app_name}/distribution_groups"
|
481
834
|
|
482
|
-
|
483
|
-
|
484
|
-
|
835
|
+
UI.message("DEBUG: GET #{url}") if ENV['DEBUG']
|
836
|
+
|
837
|
+
status, message, response = retry_429_and_error do
|
838
|
+
response = connection.get(url) do |req|
|
839
|
+
req.headers['X-API-Token'] = api_token
|
840
|
+
req.headers['internal-request-source'] = "fastlane"
|
841
|
+
end
|
485
842
|
end
|
486
843
|
|
487
|
-
case
|
844
|
+
case status
|
845
|
+
when 0, 429
|
846
|
+
if status == 0
|
847
|
+
UI.error("Faraday http fetching destribution groups: #{message}")
|
848
|
+
else
|
849
|
+
UI.error("Retryable error fetching destribution groups: #{status}: #{message}")
|
850
|
+
end
|
851
|
+
false
|
488
852
|
when 200...300
|
489
|
-
UI.message("DEBUG: #{JSON.pretty_generate(response.body)}\n") if ENV['DEBUG']
|
490
853
|
response.body
|
491
854
|
when 401
|
492
855
|
UI.user_error!("Auth Error, provided invalid token")
|
@@ -503,16 +866,26 @@ module Fastlane
|
|
503
866
|
def self.fetch_devices(api_token:, owner_name:, app_name:, distribution_group:)
|
504
867
|
connection = self.connection(nil, false, true)
|
505
868
|
|
506
|
-
|
869
|
+
url = "/v0.1/apps/#{owner_name}/#{app_name}/distribution_groups/#{ERB::Util.url_encode(distribution_group)}/devices/download_devices_list"
|
870
|
+
|
871
|
+
UI.message("DEBUG: GET #{url}") if ENV['DEBUG']
|
507
872
|
|
508
|
-
response =
|
509
|
-
|
510
|
-
|
873
|
+
status, message, response = retry_429_and_error do
|
874
|
+
response = connection.get(url) do |req|
|
875
|
+
req.headers['X-API-Token'] = api_token
|
876
|
+
req.headers['internal-request-source'] = "fastlane"
|
877
|
+
end
|
511
878
|
end
|
512
879
|
|
513
|
-
case
|
880
|
+
case status
|
881
|
+
when 0, 429
|
882
|
+
if status == 0
|
883
|
+
UI.error("Faraday http fetching devices: #{message}")
|
884
|
+
else
|
885
|
+
UI.error("Retryable error fetching devices: #{status}: #{message}")
|
886
|
+
end
|
887
|
+
false
|
514
888
|
when 200...300
|
515
|
-
UI.message("DEBUG: #{response.body.inspect}") if ENV['DEBUG']
|
516
889
|
response.body
|
517
890
|
when 401
|
518
891
|
UI.user_error!("Auth Error, provided invalid token")
|
@@ -529,40 +902,129 @@ module Fastlane
|
|
529
902
|
def self.fetch_releases(api_token:, owner_name:, app_name:)
|
530
903
|
connection = self.connection(nil, false, true)
|
531
904
|
|
532
|
-
|
905
|
+
url = "/v0.1/apps/#{owner_name}/#{app_name}/releases"
|
533
906
|
|
534
|
-
|
535
|
-
req.headers['X-API-Token'] = api_token
|
536
|
-
req.headers['internal-request-source'] = "fastlane"
|
537
|
-
end
|
907
|
+
UI.message("DEBUG: GET #{url}") if ENV['DEBUG']
|
538
908
|
|
539
|
-
|
540
|
-
|
541
|
-
|
542
|
-
|
543
|
-
|
544
|
-
UI.user_error!("Auth Error, provided invalid token")
|
545
|
-
false
|
546
|
-
when 404
|
547
|
-
UI.error("Not found, invalid owner or application name")
|
548
|
-
false
|
549
|
-
else
|
550
|
-
UI.error("Error #{response.status}: #{response.body}")
|
551
|
-
false
|
909
|
+
status, message, response = retry_429_and_error do
|
910
|
+
response = connection.get(url) do |req|
|
911
|
+
req.headers['X-API-Token'] = api_token
|
912
|
+
req.headers['internal-request-source'] = "fastlane"
|
913
|
+
end
|
552
914
|
end
|
915
|
+
|
916
|
+
case status
|
917
|
+
when 0, 429
|
918
|
+
if status == 0
|
919
|
+
UI.error("Faraday http fetching releases: #{message}")
|
920
|
+
else
|
921
|
+
UI.error("Retryable error fetching releases: #{status}: #{message}")
|
922
|
+
end
|
923
|
+
false
|
924
|
+
when 200...300
|
925
|
+
JSON.parse(response.body)
|
926
|
+
when 401
|
927
|
+
UI.user_error!("Auth Error, provided invalid token")
|
928
|
+
false
|
929
|
+
when 404
|
930
|
+
UI.error("Not found, invalid owner or application name")
|
931
|
+
false
|
932
|
+
else
|
933
|
+
UI.error("Error #{response.status}: #{response.body}")
|
934
|
+
false
|
935
|
+
end
|
553
936
|
end
|
554
937
|
|
555
|
-
# Note: This does not support testing environment (INT)
|
556
938
|
def self.get_release_url(owner_type, owner_name, app_name, release_id)
|
557
939
|
owner_path = owner_type == "user" ? "users/#{owner_name}" : "orgs/#{owner_name}"
|
940
|
+
if ENV['APPCENTER_ENV']&.upcase == 'INT'
|
941
|
+
return "https://portal-server-core-integration.dev.avalanch.es/#{owner_path}/apps/#{app_name}/distribute/releases/#{release_id}"
|
942
|
+
end
|
943
|
+
|
558
944
|
return "https://appcenter.ms/#{owner_path}/apps/#{app_name}/distribute/releases/#{release_id}"
|
559
945
|
end
|
560
946
|
|
561
|
-
# Note: This does not support testing environment (INT)
|
562
947
|
def self.get_install_url(owner_type, owner_name, app_name)
|
563
948
|
owner_path = owner_type == "user" ? "users/#{owner_name}" : "orgs/#{owner_name}"
|
949
|
+
if ENV['APPCENTER_ENV']&.upcase == 'INT'
|
950
|
+
return "https://install.portal-server-core-integration.dev.avalanch.es/#{owner_path}/apps/#{app_name}"
|
951
|
+
end
|
952
|
+
|
564
953
|
return "https://install.appcenter.ms/#{owner_path}/apps/#{app_name}"
|
565
954
|
end
|
955
|
+
|
956
|
+
# add new created app to existing distribution group
|
957
|
+
def self.add_new_app_to_distribution_group(api_token:, owner_name:, app_name:, destination_name:)
|
958
|
+
url = URI.escape("/v0.1/orgs/#{owner_name}/distribution_groups/#{destination_name}/apps")
|
959
|
+
body = {
|
960
|
+
apps: [
|
961
|
+
{ name: app_name }
|
962
|
+
]
|
963
|
+
}
|
964
|
+
|
965
|
+
UI.message("DEBUG: POST #{url}") if ENV['DEBUG']
|
966
|
+
UI.message("DEBUG: POST body #{JSON.pretty_generate(body)}\n") if ENV['DEBUG']
|
967
|
+
|
968
|
+
status, message, response = retry_429_and_error do
|
969
|
+
response = connection.post(url) do |req|
|
970
|
+
req.headers['X-API-Token'] = api_token
|
971
|
+
req.headers['internal-request-source'] = "fastlane"
|
972
|
+
req.body = body
|
973
|
+
end
|
974
|
+
end
|
975
|
+
|
976
|
+
case status
|
977
|
+
when 0, 429
|
978
|
+
if status == 0
|
979
|
+
UI.error("Faraday http adding to distribution group: #{message}")
|
980
|
+
else
|
981
|
+
UI.error("Retryable error adding to distribution group: #{status}: #{message}")
|
982
|
+
end
|
983
|
+
when 200...300
|
984
|
+
response.body
|
985
|
+
UI.success("Added new app #{app_name} to distribution group #{destination_name}")
|
986
|
+
when 401
|
987
|
+
UI.user_error!("Auth Error, provided invalid token")
|
988
|
+
when 404
|
989
|
+
UI.error("Not found, invalid distribution group name #{destination_name}")
|
990
|
+
when 409
|
991
|
+
UI.success("App already added to distribution group #{destination_name}")
|
992
|
+
else
|
993
|
+
UI.error("Error adding app to distribution group #{response.status}: #{response.body}")
|
994
|
+
end
|
995
|
+
end
|
996
|
+
|
997
|
+
def self.retry_429_and_error(&block)
|
998
|
+
retries = 0
|
999
|
+
status = 0
|
1000
|
+
|
1001
|
+
# status == 0 - Faraday error
|
1002
|
+
# status == 429 - retryable error code from server
|
1003
|
+
while ((status == 0) || (status == 429)) && (retries <= MAX_REQUEST_RETRIES)
|
1004
|
+
begin
|
1005
|
+
# calling request sending logic
|
1006
|
+
response = block.call
|
1007
|
+
|
1008
|
+
# checking reponse
|
1009
|
+
status = response.status
|
1010
|
+
message = response.body
|
1011
|
+
UI.message("DEBUG: #{status} #{JSON.pretty_generate(message)}\n") if ENV['DEBUG']
|
1012
|
+
rescue Faraday::Error => e
|
1013
|
+
status = 0
|
1014
|
+
message = e.message
|
1015
|
+
end
|
1016
|
+
|
1017
|
+
# Pause before retrying
|
1018
|
+
if (status == 0) || (status == 429)
|
1019
|
+
sleep(REQUEST_RETRY_INTERVAL)
|
1020
|
+
end
|
1021
|
+
|
1022
|
+
retries += 1
|
1023
|
+
end
|
1024
|
+
|
1025
|
+
return status, message, response
|
1026
|
+
end
|
1027
|
+
|
566
1028
|
end
|
567
1029
|
end
|
568
1030
|
end
|