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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d80a48da7efa15bbdfb68ed3af18b8ed57bac5ecca51d0725b428520a0912608
4
- data.tar.gz: 651ed034f616868690bcf677453cb6c8646c9d02692d831284b54f5fe061d4f0
3
+ metadata.gz: b110405fe9e84494ddb201d8b05738652c29493a2d86cf33a83f63e3843c650f
4
+ data.tar.gz: a6a582ac374e02c45a5285b6960d8f0da7a44bb7f4375c2d001d25a3aad71d21
5
5
  SHA512:
6
- metadata.gz: 68027e3171f663771bc1030396874f14c95f118050b8b881a6d61c2a0a9569876f7fe27a8841c32cfcb00c7a8e536fed5c26f47a64f15df78aed5b9683681cc6
7
- data.tar.gz: 8d4584eea5879862b0f08a7e074fbee368636de399dd55f76f3e585f4b44717b72f8c2d4dae3da6cbab32b6d19eb6b319a1cb7761850d3399866d167b3972993
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 (default: `No changelog given`) |
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 by using the blue Intercom button on the bottom right to start a conversation.
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 'json'
2
- require 'net/http'
3
- require 'fastlane_core/ui/ui'
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
- sorted_release = releases.sort_by { |release| release['id'] }.reverse!
31
- latest_release = sorted_release.first
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
- UI.user_error!("This app has no releases yet")
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
- "id" => latest_release['id'],
40
- "version" => latest_release['short_version'],
41
- "build_number" => latest_release['version']
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('https://api.appcenter.ms')
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['X-API-Token'] = api_token
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['upload_id']
190
- upload_url = upload_details['upload_url']
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
- uploaded = Helper::AppcenterHelper.upload_build(api_token, owner_name, app_name, file, upload_id, upload_url, timeout)
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 uploaded
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 || ENV.fetch('APPCENTER_UPLOAD_URL', "https://api.appcenter.ms")
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 = connection.post("v0.1/apps/#{owner_name}/#{app_name}/release_uploads") do |req|
46
- req.headers['X-API-Token'] = api_token
47
- req.headers['internal-request-source'] = "fastlane"
48
- req.body = body.nil? && {} || body
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 response.status
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.crash!("Internal Service Error, please try again later")
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
- response = connection.post("v0.1/apps/#{owner_name}/#{app_name}/symbol_uploads") do |req|
78
- req.headers['X-API-Token'] = api_token
79
- req.headers['internal-request-source'] = "fastlane"
80
- req.body = {
81
- symbol_type: "AndroidProguard",
82
- file_name: file_name,
83
- build: build_number,
84
- version: version,
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 response.status
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
- response = connection.post("v0.1/apps/#{owner_name}/#{app_name}/symbol_uploads") do |req|
113
- req.headers['X-API-Token'] = api_token
114
- req.headers['internal-request-source'] = "fastlane"
115
- req.body = {
116
- symbol_type: 'Apple'
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 response.status
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
- response = connection.patch("v0.1/apps/#{owner_name}/#{app_name}/symbol_uploads/#{symbol_upload_id}") do |req|
141
- req.headers['X-API-Token'] = api_token
142
- req.headers['internal-request-source'] = "fastlane"
143
- req.body = {
144
- "status" => status
145
- }
146
- end
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
- case response.status
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
- response = connection.put do |req|
168
- req.headers['x-ms-blob-type'] = "BlockBlob"
169
- req.headers['Content-Length'] = File.size(symbol).to_s
170
- req.headers['internal-request-source'] = "fastlane"
171
- req.body = Faraday::UploadIO.new(symbol, 'application/octet-stream') if symbol && File.exist?(symbol)
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
- logType = "dSYM" if (symbol_type == "Apple")
175
- logType = "mapping" if (symbol_type == "Android")
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 response.status
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("#{logType} uploaded")
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 #{logType} #{response.status}: #{response.body}")
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("#{logType} upload aborted")
275
+ UI.error("#{log_type} upload aborted")
188
276
  false
189
277
  end
190
278
  end
191
279
 
192
- # upload binary for specified upload_url
193
- # if succeed, then commits the release
194
- # otherwise aborts
195
- def self.upload_build(api_token, owner_name, app_name, file, upload_id, upload_url, timeout)
196
- connection = self.connection(upload_url)
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
- options = {}
199
- options[:upload_id] = upload_id
200
- # ipa field is used for .apk, .aab and .ipa files
201
- options[:ipa] = Faraday::UploadIO.new(file, 'application/octet-stream') if file && File.exist?(file)
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
- response = connection.post do |req|
204
- req.options.timeout = timeout
205
- req.headers['internal-request-source'] = "fastlane"
206
- req.body = options
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 response.status
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
- UI.message("Binary uploaded")
212
- self.update_release_upload(api_token, owner_name, app_name, upload_id, 'committed')
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 uploading binary #{response.status}: #{response.body}")
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
- response = connection.patch("v0.1/apps/#{owner_name}/#{app_name}/release_uploads/#{upload_id}") do |req|
229
- req.headers['X-API-Token'] = api_token
230
- req.headers['internal-request-source'] = "fastlane"
231
- req.body = {
232
- "status" => status
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 response.status
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.crash!("Internal Service Error, please try again later")
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
- response = connection.get("v0.1/apps/#{owner_name}/#{app_name}/releases/#{release_id}") do |req|
255
- req.headers['X-API-Token'] = api_token
256
- req.headers['internal-request-source'] = "fastlane"
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 response.status
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
- response = connection.get("v0.1/apps/#{owner_name}/#{app_name}/distribution_#{destination_type}s/#{ERB::Util.url_encode(destination_name)}") do |req|
281
- req.headers['X-API-Token'] = api_token
282
- req.headers['internal-request-source'] = "fastlane"
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 response.status
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
- response = connection.put("v0.1/apps/#{owner_name}/#{app_name}/releases/#{release_id}") do |req|
307
- req.headers['X-API-Token'] = api_token
308
- req.headers['internal-request-source'] = "fastlane"
309
- req.body = {
310
- release_notes: release_notes
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 response.status
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
- release_metadata = {
347
- dsa_signature: dsa_signature
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
- response = connection.patch("v0.1/apps/#{owner_name}/#{app_name}/releases/#{release_id}") do |req|
352
- req.headers['X-API-Token'] = api_token
353
- req.headers['internal-request-source'] = "fastlane"
354
- req.body = {
355
- metadata: release_metadata
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 response.status
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
- UI.message("DEBUG: getting #{release_id}") if ENV['DEBUG']
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
- response = connection.post("v0.1/apps/#{owner_name}/#{app_name}/releases/#{release_id}/#{destination_type}s") do |req|
387
- req.headers['X-API-Token'] = api_token
388
- req.headers['internal-request-source'] = "fastlane"
389
- req.body = body
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 response.status
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
- response = connection.get("v0.1/apps/#{owner_name}/#{app_name}") do |req|
425
- req.headers['X-API-Token'] = api_token
426
- req.headers['internal-request-source'] = "fastlane"
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 response.status
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
- endpoint = owner_type == "user" ? "v0.1/apps" : "v0.1/orgs/#{owner_name}/apps"
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 = connection.post(endpoint) do |req|
452
- req.headers['X-API-Token'] = api_token
453
- req.headers['internal-request-source'] = "fastlane"
454
- req.body = {
455
- "display_name" => app_display_name,
456
- "name" => app_name,
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 response.status
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
- endpoint = "/v0.1/apps/#{owner_name}/#{app_name}/distribution_groups"
833
+ url = "/v0.1/apps/#{owner_name}/#{app_name}/distribution_groups"
481
834
 
482
- response = connection.get(endpoint) do |req|
483
- req.headers['X-API-Token'] = api_token
484
- req.headers['internal-request-source'] = "fastlane"
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 response.status
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
- endpoint = "/v0.1/apps/#{owner_name}/#{app_name}/distribution_groups/#{ERB::Util.url_encode(distribution_group)}/devices/download_devices_list"
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 = connection.get(endpoint) do |req|
509
- req.headers['X-API-Token'] = api_token
510
- req.headers['internal-request-source'] = "fastlane"
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 response.status
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
- endpoint = "/v0.1/apps/#{owner_name}/#{app_name}/releases"
905
+ url = "/v0.1/apps/#{owner_name}/#{app_name}/releases"
533
906
 
534
- response = connection.get(endpoint) do |req|
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
- case response.status
540
- when 200...300
541
- UI.message("DEBUG: #{response.body.inspect}") if ENV['DEBUG']
542
- JSON.parse(response.body)
543
- when 401
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