fastlane-plugin-appcenter 1.7.0 → 1.10.0

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
- SHA1:
3
- metadata.gz: 0cf08880b9ba588e79b23dd385311f4d5a194246
4
- data.tar.gz: fe26549362f32bbd22293bc8110e432a7a94118a
2
+ SHA256:
3
+ metadata.gz: 63dc35fc59b2bd98c82d21b139557a0ce06fbdd137367391435cee95d119045d
4
+ data.tar.gz: 2aa31b4b0d0091f35a0953c89ddbe69bfc0ca6d4b05193a3654455ff1bdd27b3
5
5
  SHA512:
6
- metadata.gz: 6ffd559816032b4c385ead27b89419541746a106e63713aeef51282d1a74950dd27eaff4d1daf59db58ccfd33a3d4efa9f3a1f9ffbefc836ed82af2d7579dcf4
7
- data.tar.gz: 80b0cb6d8016fa35400e0ae7c1e7d1dfd9ac2e07cc3b6a96decb26917bf6c5988ab9a7f2fb407e4ee96e8303cd716dd6fc020785f6a2aa06f8167681f7d18e6f
6
+ metadata.gz: aa2bf15c7177cbaaa9108e1d151e82a3f145467359765caa064e1d11462554de7b018263ccf023805dab4afe52d138aa055f9657af2ab8d3e91e01149807cb2e
7
+ data.tar.gz: 1089271e589573573bf89a528ab0c97f562cf2ba3136276dcdf52fc2806f7229af9128b9c16419c011a88d1bb1fb11aa556131107d381de2141e99f98bc076d8
data/README.md CHANGED
@@ -21,6 +21,8 @@ With [App Center](https://appcenter.ms) you can continuously build, test, releas
21
21
 
22
22
  `appcenter_upload` allows you to upload and [distribute](https://docs.microsoft.com/en-us/appcenter/distribution/uploading) apps to your testers on App Center as well as to upload .dSYM files to [collect detailed crash reports](https://docs.microsoft.com/en-us/appcenter/crashes/ios) in App Center.
23
23
 
24
+ `appcenter_fetch_version_number` allows you to obtain the latest version number (short or full) for an app. This is useful for tasks such as getting the latest version of an app so that an increment action can take place on CI, or checking that an upload has been successful.
25
+
24
26
  ## Usage
25
27
 
26
28
  To get started, first, [obtain an API token](https://appcenter.ms/settings/apitokens) in App Center. The API Token is used to authenticate with the App Center API in each call.
@@ -29,7 +31,6 @@ To get started, first, [obtain an API token](https://appcenter.ms/settings/apito
29
31
  appcenter_fetch_devices(
30
32
  api_token: "<appcenter token>",
31
33
  owner_name: "<appcenter account name of the owner of the app (username or organization URL name)>",
32
- owner_type: "user", # Default is user - set to organization for appcenter organizations
33
34
  app_name: "<appcenter app name>",
34
35
  destinations: "*", # Default is 'Collaborators', use '*' for all distribution groups
35
36
  devices_file: "devices.txt" # Default. If you customize, the extension must be .txt
@@ -47,6 +48,21 @@ appcenter_upload(
47
48
  )
48
49
  ```
49
50
 
51
+ ```ruby
52
+ appcenter_fetch_version_number(
53
+ api_token: "<appcenter token>",
54
+ owner_name: "<appcenter account name of the owner of the app (username or organization URL name)>",
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
+ )
58
+ ```
59
+
60
+ The `appcenter_fetch_version_number` returns a hash that contains the id, the version number, and the build number. The version corresponds to the `short_version` and the build number to the `version` known by App Center for a given release:
61
+ ```ruby
62
+ {"id"=>1, "version"=>"1.0.0", "build_number"=>"1.0.0.1234"} # iOS apps contain the full version plus build number due to the way that Apple use CFBundleVersion for this value
63
+ {"id"=>588, "version"=>"1.2.0", "build_number"=>"1615"}
64
+ ```
65
+
50
66
  ### Help
51
67
 
52
68
  Once installed, information and help for an action can be printed out with this command:
@@ -74,7 +90,6 @@ Here is the list of all existing parameters:
74
90
  | Key & Env Var | Description |
75
91
  |-----------------|--------------------|
76
92
  | `api_token` <br/> `APPCENTER_API_TOKEN` | API Token for App Center |
77
- | `owner_type` <br/> `APPCENTER_OWNER_TYPE` | Owner type, either 'user' or 'organization' (default: `user`) |
78
93
  | `owner_name` <br/> `APPCENTER_OWNER_NAME` | Owner name, as found in the App's URL in App Center |
79
94
  | `destinations` <br/> `APPCENTER_DISTRIBUTE_DESTINATIONS` | Comma separated list of distribution group names. Default is 'Collaborators', use '*' for all distribution groups |
80
95
  | `devices_file` <br/> `FL_REGISTER_DEVICES_FILE` | File to save the devices list to. Same environment variable as _fastlane_'s `register_devices` action |
@@ -88,7 +103,7 @@ Here is the list of all existing parameters:
88
103
  | `owner_name` <br/> `APPCENTER_OWNER_NAME` | Owner name as found in the App's URL in App Center |
89
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 |
90
105
  | `app_display_name` <br/> `APPCENTER_APP_DISPLAY_NAME` | App display name to use when creating a new app |
91
- | `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 |
92
107
  | `app_platform` <br/> `APPCENTER_APP_PLATFORM` | App Platform. Used for new app creation, if app 'app_name' was not found |
93
108
  | `file` <br/> `APPCENTER_DISTRIBUTE_FILE` | File path to the release build to publish |
94
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`) |
@@ -96,7 +111,7 @@ Here is the list of all existing parameters:
96
111
  | `upload_dsym_only` <br/> `APPCENTER_DISTRIBUTE_UPLOAD_DSYM_ONLY` | Flag to upload only the dSYM file to App Center (default: `false`) |
97
112
  | `mapping` <br/> `APPCENTER_DISTRIBUTE_ANDROID_MAPPING` | Path to your Android mapping.txt |
98
113
  | `upload_mapping_only` <br/> `APPCENTER_DISTRIBUTE_UPLOAD_ANDROID_MAPPING_ONLY` | Flag to upload only the mapping.txt file to App Center (default: `false`) |
99
- | `destinations` <br/> `APPCENTER_DISTRIBUTE_DESTINATIONS` | Comma separated list of destination names. Both distribution groups and stores are supported. All names are required to be of the same destination type (default: `Collaborators`) |
114
+ | `destinations` <br/> `APPCENTER_DISTRIBUTE_DESTINATIONS` | Comma separated list of destination names, use '*' for all distribution groups if destination type is 'group'. Both distribution groups and stores are supported. All names are required to be of the same destination type (default: `Collaborators`) |
100
115
  | `destination_type` <br/> `APPCENTER_DISTRIBUTE_DESTINATION_TYPE` | Destination type of distribution destination. 'group' and 'store' are supported (default: `group`) |
101
116
  | `mandatory_update` <br/> `APPCENTER_DISTRIBUTE_MANDATORY_UPDATE` | Require users to update to this release. Ignored if destination type is 'store' (default: `false`) |
102
117
  | `notify_testers` <br/> `APPCENTER_DISTRIBUTE_NOTIFY_TESTERS` | Send email notification about release. Ignored if destination type is 'store' (default: `false`) |
@@ -105,10 +120,18 @@ Here is the list of all existing parameters:
105
120
  | `release_notes_link` <br/> `APPCENTER_DISTRIBUTE_RELEASE_NOTES_LINK` | Additional release notes link |
106
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` |
107
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` |
108
- | `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 |
109
124
  | `dsa_signature` <br/> `APPCENTER_DISTRIBUTE_DSA_SIGNATURE` | DSA signature of the macOS or Windows release for Sparkle update feed |
110
125
  | `strict` <br/> `APPCENTER_STRICT_MODE` | Strict mode, set to 'true' to fail early in case a potential error was detected |
111
126
 
127
+ #### `appcenter_fetch_version_number`
128
+
129
+ | Key & Env Var | Description |
130
+ |-----------------|--------------------|
131
+ | `api_token` <br/> `APPCENTER_API_TOKEN` | API Token for App Center |
132
+ | `owner_name` <br/> `APPCENTER_OWNER_NAME` | Owner name, as found in the App's URL in App Center |
133
+ | `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 |
134
+ | `version` <br/> `APPCENTER_APP_VERSION` | App version to get the last release for instead of the last release of all versions |
112
135
 
113
136
  ## Example
114
137
 
@@ -162,4 +185,4 @@ Check out [SECURITY.md](SECURITY.md) for any security concern with this project.
162
185
 
163
186
  ## Contact
164
187
 
165
- 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.
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. 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.
@@ -0,0 +1,103 @@
1
+ require "json"
2
+ require "net/http"
3
+ require "fastlane_core/ui/ui"
4
+
5
+ module Fastlane
6
+ UI = FastlaneCore::UI unless Fastlane.const_defined?("UI")
7
+
8
+ module Actions
9
+ class AppcenterFetchVersionNumberAction < Action
10
+ def self.description
11
+ "Fetches the latest version number of an app or the last build number of a version from App Center"
12
+ end
13
+
14
+ def self.authors
15
+ ["jspargo", "ShopKeep", "Qutaibah"]
16
+ end
17
+
18
+ def self.run(params)
19
+ api_token = params[:api_token]
20
+ app_name = params[:app_name]
21
+ owner_name = params[:owner_name]
22
+ version = params[:version]
23
+
24
+ releases = Helper::AppcenterHelper.fetch_releases(
25
+ api_token: api_token,
26
+ owner_name: owner_name,
27
+ app_name: app_name,
28
+ )
29
+
30
+ UI.abort_with_message!("No versions found for '#{app_name}' owned by #{owner_name}") unless releases
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
41
+
42
+ if latest_release.nil?
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")
48
+ return nil
49
+ end
50
+
51
+ return {
52
+ "id" => latest_release["id"],
53
+ "version" => latest_release["short_version"],
54
+ "build_number" => latest_release["version"],
55
+ }
56
+ end
57
+
58
+ def self.available_options
59
+ [
60
+ FastlaneCore::ConfigItem.new(key: :api_token,
61
+ env_name: "APPCENTER_API_TOKEN",
62
+ description: "API Token for App Center Access",
63
+ verify_block: proc do |value|
64
+ UI.user_error!("No API token for App Center given, pass using `api_token: 'token'`") unless value && !value.empty?
65
+ end),
66
+ FastlaneCore::ConfigItem.new(key: :owner_name,
67
+ env_name: "APPCENTER_OWNER_NAME",
68
+ description: "Name of the owner of the application on App Center",
69
+ verify_block: proc do |value|
70
+ UI.user_error!("No owner name for App Center given, pass using `owner_name: 'owner name'`") unless value && !value.empty?
71
+ end),
72
+ FastlaneCore::ConfigItem.new(key: :app_name,
73
+ env_name: "APPCENTER_APP_NAME",
74
+ description: "Name of the application on App Center",
75
+ verify_block: proc do |value|
76
+ UI.user_error!("No app name for App Center given, pass using `app_name: 'app name'`") unless value && !value.empty?
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
+
84
+ ]
85
+ end
86
+
87
+ def self.is_supported?(platform)
88
+ [:ios, :android].include?(platform)
89
+ end
90
+
91
+ def self.get_apps(api_token)
92
+ host_uri = URI.parse("https://api.appcenter.ms")
93
+ http = Net::HTTP.new(host_uri.host, host_uri.port)
94
+ http.use_ssl = true
95
+ apps_request = Net::HTTP::Get.new("/v0.1/apps")
96
+ apps_request["X-API-Token"] = api_token
97
+ apps_response = http.request(apps_request)
98
+ return [] unless apps_response.kind_of?(Net::HTTPOK)
99
+ return JSON.parse(apps_response.body)
100
+ end
101
+ end
102
+ end
103
+ end
@@ -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)
@@ -22,6 +39,13 @@ module Fastlane
22
39
  end
23
40
 
24
41
  class AppcenterUploadAction < Action
42
+ def self.is_apple_build(file)
43
+ return false unless file
44
+
45
+ file_ext = Helper::AppcenterHelper.file_extname_full(file)
46
+ ((Constants::SUPPORTED_EXTENSIONS[:ios] + Constants::SUPPORTED_EXTENSIONS[:mac])).include? file_ext
47
+ end
48
+
25
49
  # run whole upload process for dSYM files
26
50
  def self.run_dsym_upload(params)
27
51
  values = params.values
@@ -35,8 +59,9 @@ module Fastlane
35
59
 
36
60
  dsym_path = nil
37
61
  if dsym
38
- # we can use dsym parameter only if build file is ipa
39
- dsym_path = dsym if !file || File.extname(file) == '.ipa'
62
+ # we can use dsym parameter for all apple builds
63
+ self.optional_error("dsym parameter can only be used with Apple builds (ios, mac)") unless !file || self.is_apple_build(file)
64
+ dsym_path = dsym
40
65
  else
41
66
  # if dsym is not set, but build is ipa - check default path
42
67
  if file && File.exist?(file) && File.extname(file) == '.ipa'
@@ -145,6 +170,7 @@ module Fastlane
145
170
  self.optional_error("Can't distribute #{file_ext} to groups, please use `destination_type: 'store'`") if Constants::STORE_ONLY_EXTENSIONS.include? file_ext
146
171
  else
147
172
  self.optional_error("Can't distribute #{file_ext} to stores, please use `destination_type: 'group'`") unless Constants::STORE_SUPPORTED_EXTENSIONS.include? file_ext
173
+ UI.user_error!("The combination of `destinations: '*'` and `destination_type: 'store'` is invalid, please use `destination_type: 'group'` or explicitly specify the destinations") if destinations == "*"
148
174
  end
149
175
 
150
176
  release_upload_body = nil
@@ -171,27 +197,57 @@ module Fastlane
171
197
  File.delete zip_file
172
198
  end
173
199
  UI.message("Creating zip archive: #{zip_file}")
174
- file = Actions::ZipAction.run(path: file, output_path: zip_file)
200
+ file = Actions::ZipAction.run(path: file, output_path: zip_file, symlinks: true)
175
201
  end
176
202
 
177
203
  UI.message("Starting release upload...")
178
204
  upload_details = Helper::AppcenterHelper.create_release_upload(api_token, owner_name, app_name, release_upload_body)
179
205
  if upload_details
180
- upload_id = upload_details['upload_id']
181
- upload_url = upload_details['upload_url']
206
+ upload_id = upload_details['id']
207
+
208
+ UI.message("Setting Metadata...")
209
+ content_type = Constants::CONTENT_TYPES[File.extname(file)&.delete('.').downcase.to_sym] || "application/octet-stream"
210
+ 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}"
211
+ chunk_size = Helper::AppcenterHelper.set_release_upload_metadata(set_metadata_url, api_token, owner_name, app_name, upload_id, timeout)
212
+ UI.abort_with_message!("Upload aborted") unless chunk_size
182
213
 
183
214
  UI.message("Uploading release binary...")
184
- uploaded = Helper::AppcenterHelper.upload_build(api_token, owner_name, app_name, file, upload_id, upload_url, timeout)
215
+ upload_url = "#{upload_details['upload_domain']}/upload/upload_chunk/#{upload_details['package_asset_id']}?token=#{upload_details['url_encoded_token']}"
216
+ uploaded = Helper::AppcenterHelper.upload_build(api_token, owner_name, app_name, file, upload_id, upload_url, content_type, chunk_size, timeout)
217
+ UI.abort_with_message!("Upload aborted") unless uploaded
218
+
219
+ UI.message("Finishing release...")
220
+ finish_url = "#{upload_details['upload_domain']}/upload/finished/#{upload_details['package_asset_id']}?token=#{upload_details['url_encoded_token']}"
221
+ finished = Helper::AppcenterHelper.finish_release_upload(finish_url, api_token, owner_name, app_name, upload_id, timeout)
222
+ UI.abort_with_message!("Upload aborted") unless finished
185
223
 
186
- if uploaded
187
- release_id = uploaded['release_id']
224
+ UI.message("Waiting for release to be ready...")
225
+ release_status_url = "v0.1/apps/#{owner_name}/#{app_name}/uploads/releases/#{upload_id}"
226
+ release_id = Helper::AppcenterHelper.poll_for_release_id(api_token, release_status_url)
227
+
228
+ if release_id.is_a? Integer
188
229
  release_url = Helper::AppcenterHelper.get_release_url(owner_type, owner_name, app_name, release_id)
189
230
  UI.message("Release '#{release_id}' committed: #{release_url}")
190
231
 
191
232
  release = Helper::AppcenterHelper.update_release(api_token, owner_name, app_name, release_id, release_notes)
192
233
  Helper::AppcenterHelper.update_release_metadata(api_token, owner_name, app_name, release_id, dsa_signature)
193
234
 
194
- destinations_array = destinations.split(',')
235
+ destinations_array = []
236
+ if destinations == '*'
237
+ UI.message("Looking up all distribution groups for #{owner_name}/#{app_name}")
238
+ distribution_groups = Helper::AppcenterHelper.fetch_distribution_groups(
239
+ api_token: api_token,
240
+ owner_name: owner_name,
241
+ app_name: app_name
242
+ )
243
+
244
+ UI.abort_with_message!("Failed to list distribution groups for #{owner_name}/#{app_name}") unless distribution_groups
245
+
246
+ destinations_array = distribution_groups.map {|h| h['name'] }
247
+ else
248
+ destinations_array = destinations.split(',').map(&:strip)
249
+ end
250
+
195
251
  destinations_array.each do |destination_name|
196
252
  destination = Helper::AppcenterHelper.get_destination(api_token, owner_name, app_name, destination_type, destination_name)
197
253
  if destination
@@ -228,10 +284,11 @@ module Fastlane
228
284
  app_platform = params[:app_platform]
229
285
 
230
286
  platforms = {
231
- Android: %w[Java React-Native Xamarin],
232
- iOS: %w[Objective-C-Swift React-Native Xamarin],
287
+ Android: %w[Java React-Native Xamarin Unity],
288
+ iOS: %w[Objective-C-Swift React-Native Xamarin Unity],
233
289
  macOS: %w[Objective-C-Swift],
234
- Windows: %w[UWP WPF WinForms Unity]
290
+ Windows: %w[UWP WPF WinForms Unity],
291
+ Custom: %w[Custom]
235
292
  }
236
293
 
237
294
  begin
@@ -260,6 +317,30 @@ module Fastlane
260
317
  end
261
318
  end
262
319
 
320
+ def self.add_app_to_distribution_group_if_needed(params)
321
+ return unless params[:destination_type] == 'group' && params[:owner_type] == 'organization' && params[:destinations] != '*'
322
+
323
+ app_distribution_groups = Helper::AppcenterHelper.fetch_distribution_groups(
324
+ api_token: params[:api_token],
325
+ owner_name: params[:owner_name],
326
+ app_name: params[:app_name]
327
+ )
328
+
329
+ group_names = app_distribution_groups.map { |g| g['name'] }
330
+ destination_names = params[:destinations].split(',').map(&:strip)
331
+
332
+ destination_names.each do |destination_name|
333
+ unless group_names.include? destination_name
334
+ Helper::AppcenterHelper.add_new_app_to_distribution_group(
335
+ api_token: params[:api_token],
336
+ owner_name: params[:owner_name],
337
+ app_name: params[:app_name],
338
+ destination_name: destination_name
339
+ )
340
+ end
341
+ end
342
+ end
343
+
263
344
  def self.run(params)
264
345
  values = params.values
265
346
  upload_build_only = params[:upload_build_only]
@@ -270,10 +351,10 @@ module Fastlane
270
351
 
271
352
  # if app found or successfully created
272
353
  if self.get_or_create_app(params)
354
+ self.add_app_to_distribution_group_if_needed(params)
273
355
  release = self.run_release_upload(params) unless upload_dsym_only || upload_mapping_only
274
356
  params[:version] = release['short_version'] if release
275
357
  params[:build_number] = release['version'] if release
276
-
277
358
  self.run_dsym_upload(params) unless upload_mapping_only || upload_build_only
278
359
  self.run_mapping_upload(params) unless upload_dsym_only || upload_build_only
279
360
  end
@@ -344,7 +425,7 @@ module Fastlane
344
425
 
345
426
  FastlaneCore::ConfigItem.new(key: :app_os,
346
427
  env_name: "APPCENTER_APP_OS",
347
- description: "App OS. Used for new app creation, if app 'app_name' was not found",
428
+ description: "App OS can be Android, iOS, macOS, Windows, Custom. Used for new app creation, if app 'app_name' was not found",
348
429
  optional: true,
349
430
  type: String),
350
431
 
@@ -424,7 +505,7 @@ module Fastlane
424
505
  self.optional_error("Extension not supported: '#{file_ext}'. Supported formats for platform '#{platform}': #{accepted_formats.join ' '}") unless accepted_formats.include? file_ext
425
506
  end
426
507
  end),
427
-
508
+
428
509
  FastlaneCore::ConfigItem.new(key: :upload_build_only,
429
510
  env_name: "APPCENTER_DISTRIBUTE_UPLOAD_BUILD_ONLY",
430
511
  description: "Flag to upload only the build to App Center. Skips uploading symbols or mapping",
@@ -460,6 +541,7 @@ module Fastlane
460
541
  FastlaneCore::ConfigItem.new(key: :mapping,
461
542
  env_name: "APPCENTER_DISTRIBUTE_ANDROID_MAPPING",
462
543
  description: "Path to your Android mapping.txt",
544
+ default_value: (defined? SharedValues::GRADLE_MAPPING_TXT_OUTPUT_PATH) && Actions.lane_context[SharedValues::GRADLE_MAPPING_TXT_OUTPUT_PATH] || nil,
463
545
  optional: true,
464
546
  type: String,
465
547
  verify_block: proc do |value|
@@ -489,12 +571,11 @@ module Fastlane
489
571
 
490
572
  FastlaneCore::ConfigItem.new(key: :destinations,
491
573
  env_name: "APPCENTER_DISTRIBUTE_DESTINATIONS",
492
- description: "Comma separated list of destination names. Both distribution groups and stores are supported. All names are required to be of the same destination type",
574
+ description: "Comma separated list of destination names, use '*' for all distribution groups if destination type is 'group'. Both distribution groups and stores are supported. All names are required to be of the same destination type",
493
575
  default_value: Actions.lane_context[SharedValues::APPCENTER_DISTRIBUTE_DESTINATIONS] || "Collaborators",
494
576
  optional: true,
495
577
  type: String),
496
578
 
497
-
498
579
  FastlaneCore::ConfigItem.new(key: :destination_type,
499
580
  env_name: "APPCENTER_DISTRIBUTE_DESTINATION_TYPE",
500
581
  description: "Destination type of distribution destination. 'group' and 'store' are supported",
@@ -553,7 +634,7 @@ module Fastlane
553
634
 
554
635
  FastlaneCore::ConfigItem.new(key: :timeout,
555
636
  env_name: "APPCENTER_DISTRIBUTE_TIMEOUT",
556
- description: "Request timeout in seconds",
637
+ 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",
557
638
  optional: true,
558
639
  type: Integer),
559
640
 
@@ -608,6 +689,16 @@ module Fastlane
608
689
  release_notes: "release notes",
609
690
  notify_testers: false
610
691
  )',
692
+ 'appcenter_upload(
693
+ api_token: "...",
694
+ owner_name: "appcenter_owner",
695
+ app_name: "testing_ios_app",
696
+ file: "./app-release.ipa",
697
+ destinations: "*",
698
+ destination_type: "group",
699
+ release_notes: "release notes",
700
+ notify_testers: false
701
+ )',
611
702
  'appcenter_upload(
612
703
  api_token: "...",
613
704
  owner_name: "appcenter_owner",
@@ -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,20 @@ 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 ||= {}
44
67
 
45
- response = connection.post("v0.1/apps/#{owner_name}/#{app_name}/release_uploads") do |req|
68
+ UI.message("DEBUG: POST #{url}") if ENV['DEBUG']
69
+ UI.message("DEBUG: POST body: #{JSON.pretty_generate(body)}\n") if ENV['DEBUG']
70
+ response = connection.post(url) do |req|
46
71
  req.headers['X-API-Token'] = api_token
47
72
  req.headers['internal-request-source'] = "fastlane"
48
- req.body = body.nil? && {} || body
73
+ req.body = body
49
74
  end
50
75
 
76
+ UI.message("DEBUG: #{response.status} #{JSON.pretty_generate(response.body)}\n") if ENV['DEBUG']
51
77
  case response.status
52
78
  when 200...300
53
- UI.message("DEBUG: #{JSON.pretty_generate(response.body)}\n") if ENV['DEBUG']
54
79
  response.body
55
80
  when 401
56
81
  UI.user_error!("Auth Error, provided invalid token")
@@ -59,7 +84,7 @@ module Fastlane
59
84
  UI.error("Not found, invalid owner or application name")
60
85
  false
61
86
  when 500...600
62
- UI.crash!("Internal Service Error, please try again later")
87
+ UI.abort_with_message!("Internal Service Error, please try again later")
63
88
  else
64
89
  UI.error("Error #{response.status}: #{response.body}")
65
90
  false
@@ -74,20 +99,27 @@ module Fastlane
74
99
  def self.create_mapping_upload(api_token, owner_name, app_name, file_name, build_number, version)
75
100
  connection = self.connection
76
101
 
77
- response = connection.post("v0.1/apps/#{owner_name}/#{app_name}/symbol_uploads") do |req|
102
+ url = "v0.1/apps/#{owner_name}/#{app_name}/symbol_uploads"
103
+ body = {
104
+ symbol_type: "AndroidProguard",
105
+ file_name: file_name,
106
+ build: build_number,
107
+ version: version,
108
+ }
109
+
110
+ UI.message("DEBUG: POST #{url}") if ENV['DEBUG']
111
+ UI.message("DEBUG: POST body #{JSON.pretty_generate(body)}\n") if ENV['DEBUG']
112
+
113
+ response = connection.post(url) do |req|
78
114
  req.headers['X-API-Token'] = api_token
79
115
  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
- }
116
+ req.body = body
86
117
  end
87
118
 
119
+ UI.message("DEBUG: #{response.status} #{JSON.pretty_generate(response.body)}\n") if ENV['DEBUG']
120
+
88
121
  case response.status
89
122
  when 200...300
90
- UI.message("DEBUG: #{JSON.pretty_generate(response.body)}\n") if ENV['DEBUG']
91
123
  response.body
92
124
  when 401
93
125
  UI.user_error!("Auth Error, provided invalid token")
@@ -109,17 +141,24 @@ module Fastlane
109
141
  def self.create_dsym_upload(api_token, owner_name, app_name)
110
142
  connection = self.connection
111
143
 
112
- response = connection.post("v0.1/apps/#{owner_name}/#{app_name}/symbol_uploads") do |req|
144
+ url = "v0.1/apps/#{owner_name}/#{app_name}/symbol_uploads"
145
+ body = {
146
+ symbol_type: 'Apple'
147
+ }
148
+
149
+ UI.message("DEBUG: POST #{url}") if ENV['DEBUG']
150
+ UI.message("DEBUG: POST body #{JSON.pretty_generate(body)}\n") if ENV['DEBUG']
151
+
152
+ response = connection.post(url) do |req|
113
153
  req.headers['X-API-Token'] = api_token
114
154
  req.headers['internal-request-source'] = "fastlane"
115
- req.body = {
116
- symbol_type: 'Apple'
117
- }
155
+ req.body = body
118
156
  end
119
157
 
158
+ UI.message("DEBUG: #{response.status} #{JSON.pretty_generate(response.body)}\n") if ENV['DEBUG']
159
+
120
160
  case response.status
121
161
  when 200...300
122
- UI.message("DEBUG: #{JSON.pretty_generate(response.body)}\n") if ENV['DEBUG']
123
162
  response.body
124
163
  when 401
125
164
  UI.user_error!("Auth Error, provided invalid token")
@@ -137,17 +176,24 @@ module Fastlane
137
176
  def self.update_symbol_upload(api_token, owner_name, app_name, symbol_upload_id, status)
138
177
  connection = self.connection
139
178
 
140
- response = connection.patch("v0.1/apps/#{owner_name}/#{app_name}/symbol_uploads/#{symbol_upload_id}") do |req|
179
+ url = "v0.1/apps/#{owner_name}/#{app_name}/symbol_uploads/#{symbol_upload_id}"
180
+ body = {
181
+ status: status
182
+ }
183
+
184
+ UI.message("DEBUG: PATCH #{url}") if ENV['DEBUG']
185
+ UI.message("DEBUG: PATCH body #{JSON.pretty_generate(body)}\n") if ENV['DEBUG']
186
+
187
+ response = connection.patch(url) do |req|
141
188
  req.headers['X-API-Token'] = api_token
142
189
  req.headers['internal-request-source'] = "fastlane"
143
- req.body = {
144
- "status" => status
145
- }
190
+ req.body = body
146
191
  end
147
192
 
193
+ UI.message("DEBUG: #{response.status} #{JSON.pretty_generate(response.body)}\n") if ENV['DEBUG']
194
+
148
195
  case response.status
149
196
  when 200...300
150
- UI.message("DEBUG: #{JSON.pretty_generate(response.body)}\n") if ENV['DEBUG']
151
197
  response.body
152
198
  when 401
153
199
  UI.user_error!("Auth Error, provided invalid token")
@@ -164,6 +210,9 @@ module Fastlane
164
210
  def self.upload_symbol(api_token, owner_name, app_name, symbol, symbol_type, symbol_upload_id, upload_url)
165
211
  connection = self.connection(upload_url, true)
166
212
 
213
+ UI.message("DEBUG: PUT #{upload_url}") if ENV['DEBUG']
214
+ UI.message("DEBUG: PUT body <data>\n") if ENV['DEBUG']
215
+
167
216
  response = connection.put do |req|
168
217
  req.headers['x-ms-blob-type'] = "BlockBlob"
169
218
  req.headers['Content-Length'] = File.size(symbol).to_s
@@ -171,77 +220,183 @@ module Fastlane
171
220
  req.body = Faraday::UploadIO.new(symbol, 'application/octet-stream') if symbol && File.exist?(symbol)
172
221
  end
173
222
 
174
- logType = "dSYM" if (symbol_type == "Apple")
175
- logType = "mapping" if (symbol_type == "Android")
223
+ UI.message("DEBUG: #{response.status} #{JSON.pretty_generate(response.body)}\n") if ENV['DEBUG']
224
+
225
+ log_type = "dSYM" if symbol_type == "Apple"
226
+ log_type = "mapping" if symbol_type == "Android"
176
227
 
177
228
  case response.status
178
229
  when 200...300
179
230
  self.update_symbol_upload(api_token, owner_name, app_name, symbol_upload_id, 'committed')
180
- UI.success("#{logType} uploaded")
231
+ UI.success("#{log_type} uploaded")
181
232
  when 401
182
233
  UI.user_error!("Auth Error, provided invalid token")
183
234
  false
184
235
  else
185
- UI.error("Error uploading #{logType} #{response.status}: #{response.body}")
236
+ UI.error("Error uploading #{log_type} #{response.status}: #{response.body}")
186
237
  self.update_symbol_upload(api_token, owner_name, app_name, symbol_upload_id, 'aborted')
187
- UI.error("#{logType} upload aborted")
238
+ UI.error("#{log_type} upload aborted")
188
239
  false
189
240
  end
190
241
  end
191
242
 
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)
243
+ # sets metadata for new upload in App Center
244
+ # returns:
245
+ # chunk size
246
+ def self.set_release_upload_metadata(set_metadata_url, api_token, owner_name, app_name, upload_id, timeout)
247
+ connection = self.connection(set_metadata_url)
248
+
249
+ UI.message("DEBUG: POST #{set_metadata_url}") if ENV['DEBUG']
250
+ UI.message("DEBUG: POST body <data>\n") if ENV['DEBUG']
251
+ response = connection.post do |req|
252
+ req.options.timeout = timeout
253
+ req.headers['internal-request-source'] = "fastlane"
254
+ end
255
+ UI.message("DEBUG: #{response.status} #{JSON.pretty_generate(response.body)}\n") if ENV['DEBUG']
197
256
 
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)
257
+ case response.status
258
+ when 200...300
259
+ chunk_size = response.body['chunk_size']
260
+ unless chunk_size.is_a? Integer
261
+ UI.error("Set metadata didn't return chunk size: #{response.status}: #{response.body}")
262
+ false
263
+ else
264
+ UI.message("Metadata set")
265
+ chunk_size
266
+ end
267
+ when 401
268
+ UI.user_error!("Auth Error, provided invalid token")
269
+ false
270
+ else
271
+ UI.error("Error setting metadata: #{response.status}: #{response.body}")
272
+ false
273
+ end
274
+ end
275
+
276
+ # Verifies a successful upload to App Center
277
+ # returns:
278
+ # successful upload response body.
279
+ def self.finish_release_upload(finish_url, api_token, owner_name, app_name, upload_id, timeout)
280
+ connection = self.connection(finish_url)
202
281
 
282
+ UI.message("DEBUG: POST #{finish_url}") if ENV['DEBUG']
203
283
  response = connection.post do |req|
204
284
  req.options.timeout = timeout
205
285
  req.headers['internal-request-source'] = "fastlane"
206
- req.body = options
207
286
  end
287
+ UI.message("DEBUG: #{response.status} #{JSON.pretty_generate(response.body)}\n") if ENV['DEBUG']
208
288
 
209
289
  case response.status
210
290
  when 200...300
211
- UI.message("Binary uploaded")
212
- self.update_release_upload(api_token, owner_name, app_name, upload_id, 'committed')
291
+ if response.body['error'] == false
292
+ UI.message("Upload finished")
293
+ self.update_release_upload(api_token, owner_name, app_name, upload_id, 'uploadFinished')
294
+ else
295
+ UI.error("Error finishing upload: #{response.body['message']}")
296
+ false
297
+ end
213
298
  when 401
214
299
  UI.user_error!("Auth Error, provided invalid token")
215
300
  false
216
301
  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")
302
+ UI.error("Error finishing upload: #{response.status}: #{response.body}")
220
303
  false
221
304
  end
222
305
  end
223
306
 
307
+ # upload binary for specified upload_url
308
+ # if succeed, then commits the release
309
+ # otherwise aborts
310
+ def self.upload_build(api_token, owner_name, app_name, file, upload_id, upload_url, content_type, chunk_size, timeout)
311
+ block_number = 1
312
+
313
+ File.open(file).each_chunk(chunk_size) do |chunk|
314
+ upload_chunk_url = "#{upload_url}&block_number=#{block_number}"
315
+ retries = 0
316
+
317
+ while retries <= MAX_REQUEST_RETRIES
318
+ begin
319
+ connection = self.connection(upload_chunk_url, true)
320
+
321
+ UI.message("DEBUG: POST #{upload_chunk_url}") if ENV['DEBUG']
322
+ UI.message("DEBUG: POST body <data>\n") if ENV['DEBUG']
323
+ response = connection.post do |req|
324
+ req.options.timeout = timeout
325
+ req.headers['internal-request-source'] = "fastlane"
326
+ req.headers['Content-Length'] = chunk.length.to_s
327
+ req.headers['Content-Type'] = 'application/octet-stream'
328
+ req.body = chunk
329
+ end
330
+ UI.message("DEBUG: #{response.status} #{JSON.pretty_generate(response.body)}\n") if ENV['DEBUG']
331
+ status = response.status
332
+ message = response.body
333
+ rescue Faraday::Error => e
334
+
335
+ # Low level HTTP errors, we will retry them
336
+ status = 0
337
+ message = e.message
338
+ end
339
+
340
+ case status
341
+ when 200...300
342
+ if response.body['error'] == false
343
+ UI.message("Chunk uploaded")
344
+ block_number += 1
345
+ break
346
+ else
347
+ UI.error("Error uploading binary #{response.body['message']}")
348
+ return false
349
+ end
350
+ when 401
351
+ UI.user_error!("Auth Error, provided invalid token")
352
+ return false
353
+ when 400...407, 409...428, 430...499
354
+ UI.user_error!("Client error: #{response.status}: #{response.body}")
355
+ return false
356
+ else
357
+ if retries < MAX_REQUEST_RETRIES
358
+ UI.message("DEBUG: Retryable error uploading binary #{status}: #{message}")
359
+ retries += 1
360
+ sleep(REQUEST_RETRY_INTERVAL)
361
+ else
362
+ UI.error("Error uploading binary #{status}: #{message}")
363
+ return false
364
+ end
365
+ end
366
+ end
367
+ end
368
+ UI.message("Binary uploaded")
369
+ end
370
+
224
371
  # Commits or aborts the upload process for a release
225
372
  def self.update_release_upload(api_token, owner_name, app_name, upload_id, status)
226
373
  connection = self.connection
227
374
 
228
- response = connection.patch("v0.1/apps/#{owner_name}/#{app_name}/release_uploads/#{upload_id}") do |req|
375
+ url = "v0.1/apps/#{owner_name}/#{app_name}/uploads/releases/#{upload_id}"
376
+ body = {
377
+ upload_status: status,
378
+ id: upload_id
379
+ }
380
+
381
+ UI.message("DEBUG: PATCH #{url}") if ENV['DEBUG']
382
+ UI.message("DEBUG: PATCH body #{JSON.pretty_generate(body)}\n") if ENV['DEBUG']
383
+
384
+ response = connection.patch(url) do |req|
229
385
  req.headers['X-API-Token'] = api_token
230
386
  req.headers['internal-request-source'] = "fastlane"
231
- req.body = {
232
- "status" => status
233
- }
387
+ req.body = body
234
388
  end
235
389
 
390
+ UI.message("DEBUG: #{response.status} #{JSON.pretty_generate(response.body)}\n") if ENV['DEBUG']
391
+
236
392
  case response.status
237
393
  when 200...300
238
- UI.message("DEBUG: #{JSON.pretty_generate(response.body)}\n") if ENV['DEBUG']
239
394
  response.body
240
395
  when 401
241
396
  UI.user_error!("Auth Error, provided invalid token")
242
397
  false
243
398
  when 500...600
244
- UI.crash!("Internal Service Error, please try again later")
399
+ UI.abort_with_message!("Internal Service Error, please try again later")
245
400
  else
246
401
  UI.error("Error #{response.status}: #{response.body}")
247
402
  false
@@ -251,15 +406,21 @@ module Fastlane
251
406
  # get existing release
252
407
  def self.get_release(api_token, owner_name, app_name, release_id)
253
408
  connection = self.connection
254
- response = connection.get("v0.1/apps/#{owner_name}/#{app_name}/releases/#{release_id}") do |req|
409
+
410
+ url = "v0.1/apps/#{owner_name}/#{app_name}/releases/#{release_id}"
411
+
412
+ UI.message("DEBUG: GET #{url}") if ENV['DEBUG']
413
+
414
+ response = connection.get(url) do |req|
255
415
  req.headers['X-API-Token'] = api_token
256
416
  req.headers['internal-request-source'] = "fastlane"
257
417
  end
258
418
 
419
+ UI.message("DEBUG: #{response.status} #{JSON.pretty_generate(response.body)}\n") if ENV['DEBUG']
420
+
259
421
  case response.status
260
422
  when 200...300
261
423
  release = response.body
262
- UI.message("DEBUG: #{JSON.pretty_generate(release)}") if ENV['DEBUG']
263
424
  release
264
425
  when 404
265
426
  UI.error("Not found, invalid release url")
@@ -273,19 +434,58 @@ module Fastlane
273
434
  end
274
435
  end
275
436
 
437
+ # Polls the upload for a release id. When a release is uploaded, we have to check
438
+ # for a successful extraction before we can continue.
439
+ # returns:
440
+ # release_distinct_id
441
+ def self.poll_for_release_id(api_token, url)
442
+ connection = self.connection
443
+
444
+ while true
445
+ UI.message("DEBUG: GET #{url}") if ENV['DEBUG']
446
+ response = connection.get(url) do |req|
447
+ req.headers['X-API-Token'] = api_token
448
+ req.headers['internal-request-source'] = "fastlane"
449
+ end
450
+
451
+ UI.message("DEBUG: #{response.status} #{JSON.pretty_generate(response.body)}\n") if ENV['DEBUG']
452
+
453
+ case response.status
454
+ when 200...300
455
+ case response.body['upload_status']
456
+ when "readyToBePublished"
457
+ return response.body['release_distinct_id']
458
+ when "error"
459
+ UI.error("Error fetching release: #{response.body['error_details']}")
460
+ return false
461
+ else
462
+ sleep(RELEASE_UPLOAD_STATUS_POLL_INTERVAL)
463
+ end
464
+ else
465
+ UI.error("Error fetching information about release #{response.status}: #{response.body}")
466
+ return false
467
+ end
468
+ end
469
+ end
470
+
276
471
  # get distribution group or store
277
472
  def self.get_destination(api_token, owner_name, app_name, destination_type, destination_name)
278
473
  connection = self.connection
279
474
 
280
- response = connection.get("v0.1/apps/#{owner_name}/#{app_name}/distribution_#{destination_type}s/#{ERB::Util.url_encode(destination_name)}") do |req|
475
+ url = "v0.1/apps/#{owner_name}/#{app_name}/distribution_#{destination_type}s/#{ERB::Util.url_encode(destination_name)}"
476
+
477
+ UI.message("DEBUG: GET #{url}") if ENV['DEBUG']
478
+
479
+ response = connection.get(url) do |req|
281
480
  req.headers['X-API-Token'] = api_token
282
481
  req.headers['internal-request-source'] = "fastlane"
283
482
  end
284
483
 
484
+ UI.message("DEBUG: #{response.status} #{JSON.pretty_generate(response.body)}\n") if ENV['DEBUG']
485
+
285
486
  case response.status
286
487
  when 200...300
287
488
  destination = response.body
288
- UI.message("DEBUG: received #{destination_type} #{JSON.pretty_generate(destination)}") if ENV['DEBUG']
289
489
  destination
290
490
  when 404
291
491
  UI.error("Not found, invalid distribution #{destination_type} name")
@@ -303,14 +503,22 @@ module Fastlane
303
503
  def self.update_release(api_token, owner_name, app_name, release_id, release_notes = '')
304
504
  connection = self.connection
305
505
 
306
- response = connection.put("v0.1/apps/#{owner_name}/#{app_name}/releases/#{release_id}") do |req|
506
+ url = "v0.1/apps/#{owner_name}/#{app_name}/releases/#{release_id}"
507
+ body = {
508
+ release_notes: release_notes
509
+ }
510
+
511
+ UI.message("DEBUG: PUT #{url}") if ENV['DEBUG']
512
+ UI.message("DEBUG: PUT body #{JSON.pretty_generate(body)}\n") if ENV['DEBUG']
513
+
514
+ response = connection.put(url) do |req|
307
515
  req.headers['X-API-Token'] = api_token
308
516
  req.headers['internal-request-source'] = "fastlane"
309
- req.body = {
310
- release_notes: release_notes
311
- }
517
+ req.body = body
312
518
  end
313
519
 
520
+ UI.message("DEBUG: #{response.status} #{JSON.pretty_generate(response.body)}\n") if ENV['DEBUG']
521
+
314
522
  case response.status
315
523
  when 200...300
316
524
  # get full release info
@@ -319,8 +527,6 @@ module Fastlane
319
527
 
320
528
  download_url = release['download_url']
321
529
 
322
- UI.message("DEBUG: #{JSON.pretty_generate(release)}") if ENV['DEBUG']
323
-
324
530
  Actions.lane_context[Fastlane::Actions::SharedValues::APPCENTER_DOWNLOAD_LINK] = download_url
325
531
  Actions.lane_context[Fastlane::Actions::SharedValues::APPCENTER_BUILD_INFORMATION] = release
326
532
 
@@ -343,19 +549,25 @@ module Fastlane
343
549
  def self.update_release_metadata(api_token, owner_name, app_name, release_id, dsa_signature)
344
550
  return if dsa_signature.to_s == ''
345
551
 
346
- release_metadata = {
347
- dsa_signature: dsa_signature
552
+ url = "v0.1/apps/#{owner_name}/#{app_name}/releases/#{release_id}"
553
+ body = {
554
+ metadata: {
555
+ dsa_signature: dsa_signature
556
+ }
348
557
  }
349
558
 
559
+ UI.message("DEBUG: PATCH #{url}") if ENV['DEBUG']
560
+ UI.message("DEBUG: PATCH body #{JSON.pretty_generate(body)}\n") if ENV['DEBUG']
561
+
350
562
  connection = self.connection
351
- response = connection.patch("v0.1/apps/#{owner_name}/#{app_name}/releases/#{release_id}") do |req|
563
+ response = connection.patch(url) do |req|
352
564
  req.headers['X-API-Token'] = api_token
353
565
  req.headers['internal-request-source'] = "fastlane"
354
- req.body = {
355
- metadata: release_metadata
356
- }
566
+ req.body = body
357
567
  end
358
568
 
569
+ UI.message("DEBUG: #{response.status} #{JSON.pretty_generate(response.body)}\n") if ENV['DEBUG']
570
+
359
571
  case response.status
360
572
  when 200...300
361
573
  UI.message("Release Metadata was successfully updated for release '#{release_id}'")
@@ -370,25 +582,32 @@ module Fastlane
370
582
  false
371
583
  end
372
584
  end
373
-
585
+
374
586
  # add release to distribution group or store
375
587
  def self.add_to_destination(api_token, owner_name, app_name, release_id, destination_type, destination_id, mandatory_update = false, notify_testers = false)
376
588
  connection = self.connection
377
589
 
378
- UI.message("DEBUG: getting #{release_id}") if ENV['DEBUG']
590
+ url = "v0.1/apps/#{owner_name}/#{app_name}/releases/#{release_id}/#{destination_type}s"
591
+ body = {
592
+ id: destination_id
593
+ }
379
594
 
380
- body = { "id" => destination_id }
381
595
  if destination_type == "group"
382
596
  body["mandatory_update"] = mandatory_update
383
597
  body["notify_testers"] = notify_testers
384
598
  end
385
599
 
386
- response = connection.post("v0.1/apps/#{owner_name}/#{app_name}/releases/#{release_id}/#{destination_type}s") do |req|
600
+ UI.message("DEBUG: POST #{url}") if ENV['DEBUG']
601
+ UI.message("DEBUG: POST body #{JSON.pretty_generate(body)}\n") if ENV['DEBUG']
602
+
603
+ response = connection.post(url) do |req|
387
604
  req.headers['X-API-Token'] = api_token
388
605
  req.headers['internal-request-source'] = "fastlane"
389
606
  req.body = body
390
607
  end
391
608
 
609
+ UI.message("DEBUG: #{response.status} #{JSON.pretty_generate(response.body)}\n") if ENV['DEBUG']
610
+
392
611
  case response.status
393
612
  when 200...300
394
613
  # get full release info
@@ -397,8 +616,6 @@ module Fastlane
397
616
 
398
617
  download_url = release['download_url']
399
618
 
400
- UI.message("DEBUG: received release #{JSON.pretty_generate(release)}") if ENV['DEBUG']
401
-
402
619
  Actions.lane_context[Fastlane::Actions::SharedValues::APPCENTER_DOWNLOAD_LINK] = download_url
403
620
  Actions.lane_context[Fastlane::Actions::SharedValues::APPCENTER_BUILD_INFORMATION] = release
404
621
 
@@ -421,11 +638,17 @@ module Fastlane
421
638
  def self.get_app(api_token, owner_name, app_name)
422
639
  connection = self.connection
423
640
 
424
- response = connection.get("v0.1/apps/#{owner_name}/#{app_name}") do |req|
641
+ url = "v0.1/apps/#{owner_name}/#{app_name}"
642
+
643
+ UI.message("DEBUG: GET #{url}") if ENV['DEBUG']
644
+
645
+ response = connection.get(url) do |req|
425
646
  req.headers['X-API-Token'] = api_token
426
647
  req.headers['internal-request-source'] = "fastlane"
427
648
  end
428
649
 
650
+ UI.message("DEBUG: #{response.status} #{JSON.pretty_generate(response.body)}\n") if ENV['DEBUG']
651
+
429
652
  case response.status
430
653
  when 200...300
431
654
  UI.message("DEBUG: #{JSON.pretty_generate(response.body)}\n") if ENV['DEBUG']
@@ -446,23 +669,28 @@ module Fastlane
446
669
  def self.create_app(api_token, owner_type, owner_name, app_name, app_display_name, os, platform)
447
670
  connection = self.connection
448
671
 
449
- endpoint = owner_type == "user" ? "v0.1/apps" : "v0.1/orgs/#{owner_name}/apps"
672
+ url = owner_type == "user" ? "v0.1/apps" : "v0.1/orgs/#{owner_name}/apps"
673
+ body = {
674
+ display_name: app_display_name,
675
+ name: app_name,
676
+ os: os,
677
+ platform: platform
678
+ }
679
+
680
+ UI.message("DEBUG: POST #{url}") if ENV['DEBUG']
681
+ UI.message("DEBUG: POST body #{JSON.pretty_generate(body)}\n") if ENV['DEBUG']
450
682
 
451
- response = connection.post(endpoint) do |req|
683
+ response = connection.post(url) do |req|
452
684
  req.headers['X-API-Token'] = api_token
453
685
  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
- }
686
+ req.body = body
460
687
  end
461
688
 
689
+ UI.message("DEBUG: #{response.status} #{JSON.pretty_generate(response.body)}\n") if ENV['DEBUG']
690
+
462
691
  case response.status
463
692
  when 200...300
464
693
  created = response.body
465
- UI.message("DEBUG: #{JSON.pretty_generate(created)}") if ENV['DEBUG']
466
694
  UI.success("Created #{os}/#{platform} app with name \"#{created['name']}\" and display name \"#{created['display_name']}\" for #{owner_type} \"#{owner_name}\"")
467
695
  true
468
696
  when 401
@@ -477,16 +705,19 @@ module Fastlane
477
705
  def self.fetch_distribution_groups(api_token:, owner_name:, app_name:)
478
706
  connection = self.connection
479
707
 
480
- endpoint = "/v0.1/apps/#{owner_name}/#{app_name}/distribution_groups"
708
+ url = "/v0.1/apps/#{owner_name}/#{app_name}/distribution_groups"
481
709
 
482
- response = connection.get(endpoint) do |req|
710
+ UI.message("DEBUG: GET #{url}") if ENV['DEBUG']
711
+
712
+ response = connection.get(url) do |req|
483
713
  req.headers['X-API-Token'] = api_token
484
714
  req.headers['internal-request-source'] = "fastlane"
485
715
  end
486
716
 
717
+ UI.message("DEBUG: #{response.status} #{JSON.pretty_generate(response.body)}\n") if ENV['DEBUG']
718
+
487
719
  case response.status
488
720
  when 200...300
489
- UI.message("DEBUG: #{JSON.pretty_generate(response.body)}\n") if ENV['DEBUG']
490
721
  response.body
491
722
  when 401
492
723
  UI.user_error!("Auth Error, provided invalid token")
@@ -503,16 +734,19 @@ module Fastlane
503
734
  def self.fetch_devices(api_token:, owner_name:, app_name:, distribution_group:)
504
735
  connection = self.connection(nil, false, true)
505
736
 
506
- endpoint = "/v0.1/apps/#{owner_name}/#{app_name}/distribution_groups/#{ERB::Util.url_encode(distribution_group)}/devices/download_devices_list"
737
+ url = "/v0.1/apps/#{owner_name}/#{app_name}/distribution_groups/#{ERB::Util.url_encode(distribution_group)}/devices/download_devices_list"
507
738
 
508
- response = connection.get(endpoint) do |req|
739
+ UI.message("DEBUG: GET #{url}") if ENV['DEBUG']
740
+
741
+ response = connection.get(url) do |req|
509
742
  req.headers['X-API-Token'] = api_token
510
743
  req.headers['internal-request-source'] = "fastlane"
511
744
  end
512
745
 
746
+ UI.message("DEBUG: #{response.status} #{JSON.pretty_generate(response.body)}\n") if ENV['DEBUG']
747
+
513
748
  case response.status
514
749
  when 200...300
515
- UI.message("DEBUG: #{response.body.inspect}") if ENV['DEBUG']
516
750
  response.body
517
751
  when 401
518
752
  UI.user_error!("Auth Error, provided invalid token")
@@ -526,17 +760,87 @@ module Fastlane
526
760
  end
527
761
  end
528
762
 
529
- # Note: This does not support testing environment (INT)
763
+ def self.fetch_releases(api_token:, owner_name:, app_name:)
764
+ connection = self.connection(nil, false, true)
765
+
766
+ url = "/v0.1/apps/#{owner_name}/#{app_name}/releases"
767
+
768
+ UI.message("DEBUG: GET #{url}") if ENV['DEBUG']
769
+
770
+ response = connection.get(url) do |req|
771
+ req.headers['X-API-Token'] = api_token
772
+ req.headers['internal-request-source'] = "fastlane"
773
+ end
774
+
775
+ UI.message("DEBUG: #{response.status} #{JSON.pretty_generate(response.body)}\n") if ENV['DEBUG']
776
+
777
+ case response.status
778
+ when 200...300
779
+ JSON.parse(response.body)
780
+ when 401
781
+ UI.user_error!("Auth Error, provided invalid token")
782
+ false
783
+ when 404
784
+ UI.error("Not found, invalid owner or application name")
785
+ false
786
+ else
787
+ UI.error("Error #{response.status}: #{response.body}")
788
+ false
789
+ end
790
+ end
791
+
530
792
  def self.get_release_url(owner_type, owner_name, app_name, release_id)
531
793
  owner_path = owner_type == "user" ? "users/#{owner_name}" : "orgs/#{owner_name}"
794
+ if ENV['APPCENTER_ENV']&.upcase == 'INT'
795
+ return "https://portal-server-core-integration.dev.avalanch.es/#{owner_path}/apps/#{app_name}/distribute/releases/#{release_id}"
796
+ end
797
+
532
798
  return "https://appcenter.ms/#{owner_path}/apps/#{app_name}/distribute/releases/#{release_id}"
533
799
  end
534
800
 
535
- # Note: This does not support testing environment (INT)
536
801
  def self.get_install_url(owner_type, owner_name, app_name)
537
802
  owner_path = owner_type == "user" ? "users/#{owner_name}" : "orgs/#{owner_name}"
803
+ if ENV['APPCENTER_ENV']&.upcase == 'INT'
804
+ return "https://install.portal-server-core-integration.dev.avalanch.es/#{owner_path}/apps/#{app_name}"
805
+ end
806
+
538
807
  return "https://install.appcenter.ms/#{owner_path}/apps/#{app_name}"
539
808
  end
809
+
810
+ # add new created app to existing distribution group
811
+ def self.add_new_app_to_distribution_group(api_token:, owner_name:, app_name:, destination_name:)
812
+ url = URI.escape("/v0.1/orgs/#{owner_name}/distribution_groups/#{destination_name}/apps")
813
+ body = {
814
+ apps: [
815
+ { name: app_name }
816
+ ]
817
+ }
818
+
819
+ UI.message("DEBUG: POST #{url}") if ENV['DEBUG']
820
+ UI.message("DEBUG: POST body #{JSON.pretty_generate(body)}\n") if ENV['DEBUG']
821
+
822
+ response = connection.post(url) do |req|
823
+ req.headers['X-API-Token'] = api_token
824
+ req.headers['internal-request-source'] = "fastlane"
825
+ req.body = body
826
+ end
827
+
828
+ UI.message("DEBUG: #{response.status} #{JSON.pretty_generate(response.body)}\n") if ENV['DEBUG']
829
+
830
+ case response.status
831
+ when 200...300
832
+ created = response.body
833
+ UI.success("Added new app #{app_name} to distribution group #{destination_name}")
834
+ when 401
835
+ UI.user_error!("Auth Error, provided invalid token")
836
+ when 404
837
+ UI.error("Not found, invalid distribution group name #{destination_name}")
838
+ when 409
839
+ UI.success("App already added to distribution group #{destination_name}")
840
+ else
841
+ UI.error("Error adding app to distribution group #{response.status}: #{response.body}")
842
+ end
843
+ end
540
844
  end
541
845
  end
542
846
  end
@@ -1,5 +1,5 @@
1
1
  module Fastlane
2
2
  module Appcenter
3
- VERSION = "1.7.0"
3
+ VERSION = "1.10.0"
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fastlane-plugin-appcenter
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.7.0
4
+ version: 1.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Microsoft Corporation
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-11-15 00:00:00.000000000 Z
11
+ date: 2020-10-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -96,6 +96,20 @@ dependencies:
96
96
  version: '0'
97
97
  - !ruby/object:Gem::Dependency
98
98
  name: rubocop
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: 0.77.0
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: 0.77.0
111
+ - !ruby/object:Gem::Dependency
112
+ name: webmock
99
113
  requirement: !ruby/object:Gem::Requirement
100
114
  requirements:
101
115
  - - ">="
@@ -118,6 +132,7 @@ files:
118
132
  - README.md
119
133
  - lib/fastlane/plugin/appcenter.rb
120
134
  - lib/fastlane/plugin/appcenter/actions/appcenter_fetch_devices_action.rb
135
+ - lib/fastlane/plugin/appcenter/actions/appcenter_fetch_version_number.rb
121
136
  - lib/fastlane/plugin/appcenter/actions/appcenter_upload_action.rb
122
137
  - lib/fastlane/plugin/appcenter/helper/appcenter_helper.rb
123
138
  - lib/fastlane/plugin/appcenter/version.rb
@@ -140,8 +155,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
140
155
  - !ruby/object:Gem::Version
141
156
  version: '0'
142
157
  requirements: []
143
- rubyforge_project:
144
- rubygems_version: 2.5.2.3
158
+ rubygems_version: 3.0.3
145
159
  signing_key:
146
160
  specification_version: 4
147
161
  summary: Fastlane plugin for App Center