fastlane-plugin-sapfire 1.2.2 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b04c4cbeb06c2856f862f7d4410efeaf27cb92a9e0b7291280199be31046573c
4
- data.tar.gz: e19d2776f46af1798b2a6ce9d9c512ae0eae5937fac8128876538d43df162095
3
+ metadata.gz: 226d306173179880376ba2856ece97544e8d0fbeb1b19878a1188c77b2ebe753
4
+ data.tar.gz: ccee066ce7a4f4bad9e64e9b7939615098984439f4f8fbdf2a7cc6b27533d66b
5
5
  SHA512:
6
- metadata.gz: b7db36a7409104f795a328a2b0706e14e3a3f7ddf732d5f397ad57f6094bd894007a60d65897f5076d7add53a0abd8e0707fd1bc36c8cc0d80592861856df2b4
7
- data.tar.gz: 78e224d53850151f10099fb430f276ed2dfcc1fb92fa6b5dd47f0217c3880c235f65de35722bae2d6e1438755c123c091a5ce5f8032af0cc3f4b79cd721b51a7
6
+ metadata.gz: 792614cad248073d030c735ba6a048426382eed3a758c9bddf8292e2cd76895c7b9a4adfc469475ceff4f3c94389ced6c33b4886e98fb2f0ce1258fb9309e414
7
+ data.tar.gz: 2daccfbafee00c0d48864506744cec183e413d6e58bd40183ef30559b2330191ee5d7ba7ae185e6ae466458fd7ff9bef64f5b21156721c326ce91adbfc6c5b1d
data/README.md CHANGED
@@ -90,6 +90,11 @@ Here is the list of all available actions. Read the documentation on each one by
90
90
  |--------------------------------------------------------|-----------------------------------------------------------------------------|--------------------:|
91
91
  | [app_certification](docs/actions/app_certification.md) | Runs Windows App Certification Kit to ensure your app is safe and efficient | windows |
92
92
 
93
+ #### 🐛 Beta
94
+
95
+ | Argument | Description | Supported platforms |
96
+ |----------------------------------------------------------------|------------------------------------------------------------------------|--------------------:|
97
+ | [upload_package_flight](docs/actions/upload_package_flight.md) | Uploads new binary to Microsoft Partner Center as a new package flight | windows |
93
98
 
94
99
  #### 📦 Releasing
95
100
 
@@ -120,14 +120,20 @@ module Fastlane
120
120
 
121
121
  if response.status == 200
122
122
  data = JSON.parse(response.body)
123
+ failure_code = data["FailureCode"]
124
+ failure_reason = data["FailureReason"]
125
+
126
+ UI.user_error!("Request returned the error.\nFailure code: #{failure_code}\nFailure reason: #{failure_reason}") if
127
+ (!failure_code.nil? && failure_code != 0) || !failure_reason.nil?
128
+
123
129
  developer_info = DeveloperInfo.new(data["PublisherDisplayName"], data["Publisher"])
124
130
 
125
- UI.message("Developer info was obtained")
131
+ UI.success("Developer info was obtained")
126
132
 
127
133
  return developer_info
128
134
  end
129
135
 
130
- UI.user_error!("Request returned the error: #{response.status}")
136
+ UI.user_error!("Request completed with non successful status: #{response.status}")
131
137
  rescue StandardError => ex
132
138
  UI.user_error!("Developer info request failed: #{ex}")
133
139
  end
@@ -151,15 +157,21 @@ module Fastlane
151
157
 
152
158
  if response.status == 200
153
159
  data = JSON.parse(response.body)
160
+ failure_code = data["FailureCode"]
161
+ failure_reason = data["FailureReason"]
162
+
163
+ UI.user_error!("Request returned the error.\nFailure code: #{failure_code}\nFailure reason: #{failure_reason}") if
164
+ (!failure_code.nil? && failure_code != 0) || !failure_reason.nil?
165
+
154
166
  product = data["Products"].find { |x| x["LandingUrl"].include?(app_id) }
155
167
  app_info = AppInfo.new(product["MainPackageIdentityName"], product["ReservedNames"])
156
168
 
157
- UI.message("Application info was obtained")
169
+ UI.success("Application info was obtained")
158
170
 
159
171
  return app_info
160
172
  end
161
173
 
162
- UI.user_error!("Request returned the error: #{response.code}")
174
+ UI.user_error!("Request completed with non successful status: #{response.status}")
163
175
  rescue StandardError => ex
164
176
  UI.user_error!("Application info request failed: #{ex}")
165
177
  end
@@ -190,7 +202,7 @@ module Fastlane
190
202
 
191
203
  if response.status == 200
192
204
  @token = data["access_token"]
193
- UI.message("Authorization token was obtained")
205
+ UI.success("Authorization token was obtained")
194
206
 
195
207
  return
196
208
  end
@@ -205,19 +217,22 @@ module Fastlane
205
217
  end
206
218
 
207
219
  def self.acquire_dev_center_location
220
+ UI.message("Acquiring Dev Center location ...")
208
221
  location = acquire_fw_url(DEV_CENTER_FW_LINK)
209
- UI.message("Dev Center location: #{location}")
222
+ UI.success("URL was obtained: #{location}")
210
223
 
211
224
  location
212
225
  end
213
226
 
214
227
  def self.acquire_vs_api_location
228
+ UI.message("Acquiring VS API location ...")
229
+
215
230
  location = acquire_fw_url(VS_API_FW_LINK)
216
231
  uri = URI(location)
217
232
  @vsapi_host = "#{uri.scheme}://#{uri.host}"
218
233
  @vsapi_endpoint = uri.path
219
234
 
220
- UI.message("VS API location: #{location}")
235
+ UI.success("URL was obtained: #{location}")
221
236
  end
222
237
 
223
238
  def self.acquire_fw_url(link_id)
@@ -235,7 +250,7 @@ module Fastlane
235
250
  return response.headers["Location"]
236
251
  end
237
252
 
238
- UI.user_error!("Request returned the error: #{response.status}")
253
+ UI.user_error!("Request completed with non successful status: #{response.status}")
239
254
  rescue StandardError => ex
240
255
  UI.user_error!("Failed to get VS API endpoint location: #{ex}")
241
256
  end
@@ -1,6 +1,7 @@
1
1
  require "fastlane/action"
2
2
  require_relative "../helper/ms_credentials"
3
3
  require_relative "../helper/ms_devcenter_helper"
4
+ require_relative "../helper/azure_blob_helper"
4
5
  require_relative "../msbuild/options"
5
6
 
6
7
  module Fastlane
@@ -22,7 +23,8 @@ module Fastlane
22
23
  UI.message("Authorization token was obtained")
23
24
 
24
25
  UI.message("Creating submission for app #{app_id} ...")
25
- pending_submission = Helper::MsDevCenterHelper.non_published_submission(app_id, auth_token, timeout)
26
+ app_info = get_app_info(app_id, auth_token, timeout)
27
+ pending_submission = app_info["pendingApplicationSubmission"]
26
28
 
27
29
  unless pending_submission.nil?
28
30
  submission_id = pending_submission["id"]
@@ -46,11 +48,11 @@ module Fastlane
46
48
  UI.message("Submission #{submission_id} created")
47
49
 
48
50
  UI.message("Prepare ZIP blob for upload ...")
49
- zip_path = create_blob_zip(File.expand_path(path))
51
+ zip_path = Helper::AzureBlobHelper.create_blob_zip(File.expand_path(path))
50
52
  UI.success("Blob is ready")
51
53
 
52
54
  UI.message("Uploading ZIP blob ...")
53
- Helper::MsDevCenterHelper.upload_blob(submission_obj["fileUploadUrl"], zip_path, timeout)
55
+ Helper::AzureBlobHelper.upload_blob(submission_obj["fileUploadUrl"], zip_path, timeout)
54
56
  UI.success("ZIP blob uploaded successfully")
55
57
 
56
58
  submission_obj = prepare_empty_submission(submission_obj)
@@ -61,11 +63,11 @@ module Fastlane
61
63
  UI.message("Updated successfully")
62
64
 
63
65
  UI.message("Committing ...")
64
- Helper::MsDevCenterHelper.commit_submission(app_id, submission_id, auth_token, timeout)
66
+ Helper::MsDevCenterHelper.commit_submission(app_id, nil, submission_id, auth_token, timeout)
65
67
 
66
68
  if params.values.include?(:skip_waiting_for_build_processing) &&
67
69
  [true].include?(params[:skip_waiting_for_build_processing])
68
- UI.success("Submission passed, but build processing were skipped. Check the Dev Center page.")
70
+ UI.success("Submission passed, but build processing were skipped. Check the Dev Center page to get an actual status.")
69
71
  return
70
72
  end
71
73
 
@@ -73,7 +75,7 @@ module Fastlane
73
75
  data = nil
74
76
  until status
75
77
  UI.message("Waiting for the submission to change the status - this may take a few minutes")
76
- data = Helper::MsDevCenterHelper.get_submission_status(app_id, submission_id, auth_token, timeout)
78
+ data = Helper::MsDevCenterHelper.get_submission_status(app_id, nil, submission_id, auth_token, timeout)
77
79
  status = data["status"] != "CommitStarted"
78
80
  sleep(30) unless status
79
81
  end
@@ -182,20 +184,6 @@ module Fastlane
182
184
  :production
183
185
  end
184
186
 
185
- def self.create_blob_zip(package_path)
186
- zip_path = File.join(File.dirname(package_path), "blob.zip")
187
- File.delete(zip_path) if File.exist?(zip_path)
188
-
189
- Zip::File.open(zip_path, create: true) do |file|
190
- file.add(File.basename(package_path), package_path)
191
-
192
- screenshot_path = File.join(Helper::SapfireHelper.root_plugin_location, "assets", "ms_example_screenshot.png")
193
- file.add("ms_example_screenshot.png", File.expand_path(screenshot_path))
194
- end
195
-
196
- zip_path
197
- end
198
-
199
187
  def self.add_package_to_submission(submission_obj, file_name)
200
188
  check_submission(submission_obj)
201
189
  UI.user_error!("Package file name can't be null or empty") if file_name.nil? || file_name.empty?
@@ -0,0 +1,233 @@
1
+ require "fastlane/action"
2
+ require_relative "../helper/ms_credentials"
3
+ require_relative "../helper/ms_devcenter_helper"
4
+ require_relative "../helper/azure_blob_helper"
5
+ require_relative "../msbuild/options"
6
+
7
+ module Fastlane
8
+ module Actions
9
+ class UploadPackageFlightAction < Action
10
+ DEFAULT_TIMEOUT = 300
11
+
12
+ def self.run(params)
13
+ ms_credentials = Helper.ms_credentials
14
+ app_id = params[:app_id]
15
+ flight_name = params[:name]
16
+ group_ids = params[:group_ids]
17
+ path = params[:path]
18
+ timeout = params.values.include?(:timeout) && params[:timeout].positive? ? params[:timeout] : DEFAULT_TIMEOUT
19
+
20
+ UI.message("Acquiring authorization token for DevCenter ...")
21
+ auth_token = Helper::MsDevCenterHelper.acquire_authorization_token(ms_credentials.tenant_id,
22
+ ms_credentials.client_id,
23
+ ms_credentials.client_secret,
24
+ timeout)
25
+ UI.message("Authorization token was obtained")
26
+ UI.message("Creating package flight for app #{app_id} ...")
27
+
28
+ flight_obj = Helper::MsDevCenterHelper.create_flight(app_id, flight_name, group_ids, auth_token, timeout)
29
+ flight_id = flight_obj["flightId"]
30
+ flight_name = flight_obj["friendlyName"]
31
+ submission_id = flight_obj["pendingFlightSubmission"]["id"]
32
+ submission_obj = Helper::MsDevCenterHelper.get_submission(app_id, flight_id, submission_id, auth_token, timeout)
33
+ UI.message("Flight #{flight_name} (ID: #{flight_id}) created")
34
+
35
+ UI.message("Prepare ZIP blob for upload ...")
36
+ zip_path = Helper::AzureBlobHelper.create_blob_zip(File.expand_path(path))
37
+ UI.success("Blob is ready")
38
+
39
+ UI.message("Uploading ZIP blob ...")
40
+ Helper::AzureBlobHelper.upload_blob(submission_obj["fileUploadUrl"], zip_path, timeout)
41
+ UI.success("ZIP blob uploaded successfully")
42
+
43
+ publish_immediate = params.values.include?(:publish_immediate) && [true].include?(params[:publish_immediate])
44
+ submission_obj = prepare_empty_submission(submission_obj, publish_immediate)
45
+ submission_obj = add_package_to_submission(submission_obj, File.basename(path))
46
+
47
+ UI.message("Updating submission data ...")
48
+ Helper::MsDevCenterHelper.update_submission(app_id, submission_obj, auth_token, timeout)
49
+ UI.message("Updated successfully")
50
+
51
+ UI.message("Committing ...")
52
+ Helper::MsDevCenterHelper.commit_submission(app_id, flight_id, submission_id, auth_token, timeout)
53
+
54
+ if params.values.include?(:skip_waiting_for_build_processing) &&
55
+ [true].include?(params[:skip_waiting_for_build_processing])
56
+ UI.success("Submission passed, but build processing were skipped. Check the Dev Center page to get an actual status.")
57
+ return
58
+ end
59
+
60
+ status = false
61
+ data = nil
62
+ until status
63
+ UI.message("Waiting for the submission to change the status - this may take a few minutes")
64
+ data = Helper::MsDevCenterHelper.get_submission_status(app_id, flight_id, submission_id, auth_token, timeout)
65
+ status = data["status"] != "CommitStarted"
66
+ sleep(30) unless status
67
+ end
68
+
69
+ if data["status"] == "CommitFailed"
70
+ errors = data["statusDetails"]["errors"]
71
+ if errors.length == 1 && errors[0]["code"] == "InvalidState"
72
+ UI.important(
73
+ [
74
+ "All submission operations passed correctly, but there are some things that you need to proceed using DevCenter.",
75
+ "Message: #{errors[0]["details"]}"
76
+ ].join("\n")
77
+ )
78
+ return
79
+ end
80
+
81
+ errors.each do |error|
82
+ UI.error("Error code: #{error["code"]}\nMessage: #{error["details"]}")
83
+ end
84
+
85
+ UI.user_error!("Submission failed")
86
+ end
87
+
88
+ UI.success("Submission passed. Check the DevCenter page.")
89
+ end
90
+
91
+ def self.description
92
+ "Creates a new package flight submission in Microsoft Partner Center and uploads new binary to it"
93
+ end
94
+
95
+ def self.authors
96
+ ["CheeryLee"]
97
+ end
98
+
99
+ def self.is_supported?(platform)
100
+ [:windows].include?(platform)
101
+ end
102
+
103
+ def self.output
104
+ [
105
+ ["SF_PUSHING_TIMEOUT", "The timeout for pushing to a server in seconds"],
106
+ ["SF_APP_ID", "The Microsoft Store ID of an application"],
107
+ ["SF_FLIGHT_NAME", "An optional name that would be used as a flight name"],
108
+ ["SF_PACKAGE", "The file path to the package to be uploaded"]
109
+ ]
110
+ end
111
+
112
+ def self.available_options
113
+ [
114
+ FastlaneCore::ConfigItem.new(
115
+ key: :timeout,
116
+ env_name: "SF_PUSHING_TIMEOUT",
117
+ description: "The timeout for pushing to a server in seconds",
118
+ optional: true,
119
+ default_value: 0,
120
+ type: Integer
121
+ ),
122
+ FastlaneCore::ConfigItem.new(
123
+ key: :app_id,
124
+ env_name: "SF_APP_ID",
125
+ description: "The Microsoft Store ID of an application",
126
+ optional: false,
127
+ type: String,
128
+ verify_block: proc do |value|
129
+ UI.user_error!("The Microsoft Store ID can't be empty") unless value && !value.empty?
130
+ end
131
+ ),
132
+ FastlaneCore::ConfigItem.new(
133
+ key: :skip_waiting_for_build_processing,
134
+ description: "If set to true, the action will only commit the submission and skip the remaining build validation",
135
+ optional: true,
136
+ default_value: false,
137
+ type: Fastlane::Boolean
138
+ ),
139
+ FastlaneCore::ConfigItem.new(
140
+ key: :publish_immediate,
141
+ description: "If set to true, the submission will be published automatically once the validation passes",
142
+ optional: true,
143
+ default_value: false,
144
+ type: Fastlane::Boolean
145
+ ),
146
+ FastlaneCore::ConfigItem.new(
147
+ key: :name,
148
+ env_name: "SF_FLIGHT_NAME",
149
+ description: "An optional name that would be used as a flight name",
150
+ optional: true,
151
+ default_value: "",
152
+ type: String
153
+ ),
154
+ FastlaneCore::ConfigItem.new(
155
+ key: :group_ids,
156
+ description: "A list of tester groups who will get a new package",
157
+ optional: false,
158
+ type: Array,
159
+ verify_block: proc do |array|
160
+ UI.user_error!("List of tester groups can't be empty") if array.empty?
161
+
162
+ array.each do |value|
163
+ UI.user_error!("Tester group ID must be a string and can't be null or empty") if
164
+ !value.is_a?(String) || value.nil? || value.empty?
165
+ end
166
+ end
167
+ ),
168
+ FastlaneCore::ConfigItem.new(
169
+ key: :path,
170
+ env_name: "SF_PACKAGE",
171
+ description: "The file path to the package to be uploaded",
172
+ optional: false,
173
+ type: String,
174
+ verify_block: proc do |value|
175
+ UI.user_error!("Path to UWP package is invalid") unless value && !value.empty?
176
+
177
+ format_valid = false
178
+ Msbuild::Options::PACKAGE_FORMATS.each do |extension|
179
+ if value.end_with?(extension)
180
+ format_valid = true
181
+ break
182
+ end
183
+ end
184
+
185
+ UI.user_error!("The provided path doesn't point to UWP file") unless
186
+ File.exist?(File.expand_path(value)) && format_valid
187
+ end
188
+ )
189
+ ]
190
+ end
191
+
192
+ def self.category
193
+ :beta
194
+ end
195
+
196
+ def self.add_package_to_submission(submission_obj, file_name)
197
+ check_submission(submission_obj)
198
+ UI.user_error!("Package file name can't be null or empty") if file_name.nil? || file_name.empty?
199
+
200
+ key = "flightPackages"
201
+ package = {
202
+ "fileName": file_name,
203
+ "fileStatus": "PendingUpload",
204
+ "minimumDirectXVersion": "None",
205
+ "minimumSystemRam": "None"
206
+ }
207
+
208
+ if submission_obj[key].empty?
209
+ submission_obj[key] = []
210
+ else
211
+ submission_obj[key].each do |existing_package|
212
+ existing_package["fileStatus"] = "PendingDelete"
213
+ end
214
+ end
215
+
216
+ submission_obj[key].append(package)
217
+ submission_obj
218
+ end
219
+
220
+ def self.prepare_empty_submission(submission_obj, publish_immediate)
221
+ check_submission(submission_obj)
222
+ submission_obj["targetPublishMode"] = publish_immediate ? "Immediate" : "Manual"
223
+ submission_obj
224
+ end
225
+
226
+ def self.check_submission(submission_obj)
227
+ UI.user_error!("Submission data object need to be provided") if submission_obj.nil?
228
+ end
229
+
230
+ private_constant(:DEFAULT_TIMEOUT)
231
+ end
232
+ end
233
+ end
@@ -0,0 +1,143 @@
1
+ module Fastlane
2
+ module Helper
3
+ class AzureBlobHelper
4
+ FILE_CHUNK_SIZE = 26_214_400
5
+ UPLOAD_RETRIES = 3
6
+
7
+ def self.create_blob_zip(package_path)
8
+ zip_path = File.join(File.dirname(package_path), "blob.zip")
9
+ File.delete(zip_path) if File.exist?(zip_path)
10
+
11
+ Zip::File.open(zip_path, create: true) do |file|
12
+ file.add(File.basename(package_path), package_path)
13
+
14
+ screenshot_path = File.join(Helper::SapfireHelper.root_plugin_location, "assets", "ms_example_screenshot.png")
15
+ file.add("ms_example_screenshot.png", File.expand_path(screenshot_path))
16
+ end
17
+
18
+ zip_path
19
+ end
20
+
21
+ def self.upload_blob(url, zip_path, timeout = 0)
22
+ UI.user_error!("File upload URL need to be provided") if !url.is_a?(String) || url.nil? || url.empty?
23
+ UI.user_error!("File path is invalid") if !zip_path.is_a?(String) || zip_path.nil? || zip_path.empty?
24
+
25
+ expand_path = File.expand_path(zip_path)
26
+ UI.user_error!("The provided path doesn't point to ZIP file") unless File.exist?(expand_path) && zip_path.end_with?(".zip")
27
+
28
+ File.open(expand_path) do |file|
29
+ block_list = []
30
+ chunks_count = (file.size.to_f / FILE_CHUNK_SIZE).ceil
31
+ current_chunk = 1
32
+
33
+ until file.eof?
34
+ bytes = file.read(FILE_CHUNK_SIZE)
35
+ id = SecureRandom.uuid.delete("-")
36
+ block_list.append(id)
37
+ retry_count = 0
38
+ result = false
39
+
40
+ UI.message("Upload chunk [#{current_chunk} / #{chunks_count}]")
41
+
42
+ while !result && retry_count < UPLOAD_RETRIES
43
+ result = upload_block(url, bytes, id, timeout)
44
+ retry_count += 1
45
+ end
46
+
47
+ UI.user_error!("Uploading failed: some chunks have not been uploaded") unless result
48
+ current_chunk += 1
49
+ end
50
+
51
+ result = upload_block_list(url, block_list, timeout)
52
+ UI.user_error!("Uploading failed: block list hasn't been uploaded") unless result
53
+ end
54
+ end
55
+
56
+ def self.upload_block(url, bytes, id, timeout = 0)
57
+ headers = {
58
+ "Content-Length": bytes.length.to_s
59
+ }
60
+
61
+ url_data = parse_upload_url(url)
62
+ url_data[:query]["comp"] = "block"
63
+ url_data[:query]["blockid"] = id
64
+ connection = Faraday.new(url_data[:host])
65
+
66
+ begin
67
+ response = connection.put(url_data[:path]) do |req|
68
+ req.headers = headers
69
+ req.params = url_data[:query]
70
+ req.body = bytes
71
+ req.options.timeout = timeout if timeout.positive?
72
+ end
73
+
74
+ return true if response.status == 201
75
+
76
+ error = response.body.to_s
77
+ UI.error("Upload request failed.\nCode: #{response.status}\nError: #{error}")
78
+ rescue StandardError => ex
79
+ UI.error("Upload request failed: #{ex}")
80
+ end
81
+
82
+ false
83
+ end
84
+
85
+ def self.upload_block_list(url, list, timeout = 0)
86
+ document = REXML::Document.new
87
+ document.xml_decl.version = "1.0"
88
+ document.xml_decl.encoding = "utf-8"
89
+ block_list = document.add_element("BlockList")
90
+
91
+ list.each do |block|
92
+ block_list.add_element("Latest").text = block
93
+ end
94
+
95
+ url_data = parse_upload_url(url)
96
+ url_data[:query]["comp"] = "blocklist"
97
+ connection = Faraday.new(url_data[:host])
98
+
99
+ begin
100
+ response = connection.put(url_data[:path]) do |req|
101
+ req.params = url_data[:query]
102
+ req.body = document.to_s
103
+ req.options.timeout = timeout if timeout.positive?
104
+ end
105
+
106
+ return true if response.status == 201
107
+
108
+ error = response.body.to_s
109
+ UI.error("Upload block list request failed.\nCode: #{response.status}\nError: #{error}")
110
+ rescue StandardError => ex
111
+ UI.error("Upload block list request failed: #{ex}")
112
+ end
113
+
114
+ false
115
+ end
116
+
117
+ def self.parse_upload_url(url)
118
+ url = URI.parse(url)
119
+ query_parts = {}
120
+ url.query.split("&").each do |x|
121
+ parts = x.split("=")
122
+ query_parts[parts[0]] = CGI.unescape(parts[1])
123
+ end
124
+
125
+ {
126
+ host: "https://#{url.host}",
127
+ path: url.path,
128
+ query: query_parts
129
+ }
130
+ end
131
+
132
+ public_class_method(:create_blob_zip)
133
+ public_class_method(:upload_blob)
134
+
135
+ private_class_method(:upload_block)
136
+ private_class_method(:upload_block_list)
137
+ private_class_method(:parse_upload_url)
138
+
139
+ private_constant(:FILE_CHUNK_SIZE)
140
+ private_constant(:UPLOAD_RETRIES)
141
+ end
142
+ end
143
+ end
@@ -7,112 +7,42 @@ module Fastlane
7
7
  REQUEST_HEADERS = {
8
8
  "Accept": "application/json"
9
9
  }.freeze
10
- FILE_CHUNK_SIZE = 26_214_400
11
- UPLOAD_RETRIES = 3
12
10
 
13
- def self.upload_blob(url, zip_path, timeout = 0)
14
- UI.user_error!("File upload URL need to be provided") if !url.is_a?(String) || url.nil? || url.empty?
15
- UI.user_error!("File path is invalid") if !zip_path.is_a?(String) || zip_path.nil? || zip_path.empty?
16
-
17
- expand_path = File.expand_path(zip_path)
18
- UI.user_error!("The provided path doesn't point to ZIP file") unless File.exist?(expand_path) && zip_path.end_with?(".zip")
19
-
20
- File.open(expand_path) do |file|
21
- block_list = []
22
- chunks_count = (file.size.to_f / FILE_CHUNK_SIZE).ceil
23
- current_chunk = 1
24
-
25
- until file.eof?
26
- bytes = file.read(FILE_CHUNK_SIZE)
27
- id = SecureRandom.uuid.delete("-")
28
- block_list.append(id)
29
- retry_count = 0
30
- result = false
31
-
32
- UI.message("Upload chunk [#{current_chunk} / #{chunks_count}]")
33
-
34
- while !result && retry_count < UPLOAD_RETRIES
35
- result = upload_block(url, bytes, id, timeout)
36
- retry_count += 1
37
- end
38
-
39
- UI.user_error!("Uploading failed: some chunks have not been uploaded") unless result
40
- current_chunk += 1
41
- end
42
-
43
- result = upload_block_list(url, block_list, timeout)
44
- UI.user_error!("Uploading failed: block list hasn't been uploaded") unless result
45
- end
46
- end
47
-
48
- def self.upload_block(url, bytes, id, timeout = 0)
49
- headers = {
50
- "Content-Length": bytes.length.to_s
51
- }
52
-
53
- url_data = parse_upload_url(url)
54
- url_data[:query]["comp"] = "block"
55
- url_data[:query]["blockid"] = id
56
- connection = Faraday.new(url_data[:host])
57
-
58
- begin
59
- response = connection.put(url_data[:path]) do |req|
60
- req.headers = headers
61
- req.params = url_data[:query]
62
- req.body = bytes
63
- req.options.timeout = timeout if timeout.positive?
64
- end
65
-
66
- return true if response.status == 201
67
-
68
- error = response.body.to_s
69
- UI.error("Upload request failed.\nCode: #{response.status}\nError: #{error}")
70
- rescue StandardError => ex
71
- UI.error("Upload request failed: #{ex}")
72
- end
73
-
74
- false
75
- end
76
-
77
- def self.upload_block_list(url, list, timeout = 0)
78
- document = REXML::Document.new
79
- document.xml_decl.version = "1.0"
80
- document.xml_decl.encoding = "utf-8"
81
- block_list = document.add_element("BlockList")
82
-
83
- list.each do |block|
84
- block_list.add_element("Latest").text = block
85
- end
11
+ def self.get_app_info(app_id, auth_token, timeout = 0)
12
+ check_app_id(app_id)
86
13
 
87
- url_data = parse_upload_url(url)
88
- url_data[:query]["comp"] = "blocklist"
89
- connection = Faraday.new(url_data[:host])
14
+ connection = Faraday.new(HOST)
15
+ url = build_url_root(app_id)
90
16
 
91
17
  begin
92
- response = connection.put(url_data[:path]) do |req|
93
- req.params = url_data[:query]
94
- req.body = document.to_s
18
+ response = connection.get(url) do |req|
19
+ req.headers = build_headers(auth_token)
95
20
  req.options.timeout = timeout if timeout.positive?
96
21
  end
22
+ data = JSON.parse(response.body)
97
23
 
98
- return true if response.status == 201
24
+ return data if response.status == 200
99
25
 
100
- error = response.body.to_s
101
- UI.error("Upload block list request failed.\nCode: #{response.status}\nError: #{error}")
26
+ UI.user_error!("Getting app request returned the error.\nCode: #{response.status}")
102
27
  rescue StandardError => ex
103
- UI.error("Upload block list request failed: #{ex}")
28
+ UI.user_error!("Getting app info process failed: #{ex}")
104
29
  end
105
-
106
- false
107
30
  end
108
31
 
109
- def self.get_app_info(app_id, auth_token, timeout = 0)
32
+ def self.get_submission(app_id, flight_id, submission_id, auth_token, timeout = 0)
110
33
  check_app_id(app_id)
34
+ check_submission_id(submission_id)
35
+
36
+ is_flight = !flight_id.nil? && !flight_id.empty?
37
+ check_flight_id(flight_id) if is_flight
111
38
 
112
39
  connection = Faraday.new(HOST)
40
+ url = build_url_root(app_id)
41
+ url += "/flights/#{flight_id}" if is_flight
42
+ url += "/submissions/#{submission_id}"
113
43
 
114
44
  begin
115
- response = connection.get("/#{API_VERSION}/#{API_ROOT}/#{app_id}") do |req|
45
+ response = connection.get(url) do |req|
116
46
  req.headers = build_headers(auth_token)
117
47
  req.options.timeout = timeout if timeout.positive?
118
48
  end
@@ -120,9 +50,12 @@ module Fastlane
120
50
 
121
51
  return data if response.status == 200
122
52
 
123
- UI.user_error!("Getting app request returned the error.\nCode: #{response.status}")
53
+ code = data["code"]
54
+ message = data["message"]
55
+
56
+ UI.user_error!("Getting flight submission request returned the error.\nCode: #{response.status} #{code}.\nDescription: #{message}")
124
57
  rescue StandardError => ex
125
- UI.user_error!("Getting app info process failed: #{ex}")
58
+ UI.user_error!("Getting flight submission process failed: #{ex}")
126
59
  end
127
60
  end
128
61
 
@@ -130,9 +63,10 @@ module Fastlane
130
63
  check_app_id(app_id)
131
64
 
132
65
  connection = Faraday.new(HOST)
66
+ url = "#{build_url_root(app_id)}/submissions"
133
67
 
134
68
  begin
135
- response = connection.post("/#{API_VERSION}/#{API_ROOT}/#{app_id}/submissions") do |req|
69
+ response = connection.post(url) do |req|
136
70
  req.headers = build_headers(auth_token)
137
71
  req.options.timeout = timeout if timeout.positive?
138
72
  end
@@ -153,11 +87,15 @@ module Fastlane
153
87
  check_app_id(app_id)
154
88
  UI.user_error!("Submission data object need to be provided") if submission_obj.nil?
155
89
 
156
- submission_id = submission_obj["id"]
157
90
  connection = Faraday.new(HOST)
91
+ flight_id = submission_obj["flightId"]
92
+ submission_id = submission_obj["id"]
93
+ url = build_url_root(app_id)
94
+ url += "/flights/#{flight_id}" if !flight_id.nil? && !flight_id.empty?
95
+ url += "/submissions/#{submission_id}"
158
96
 
159
97
  begin
160
- response = connection.put("/#{API_VERSION}/#{API_ROOT}/#{app_id}/submissions/#{submission_id}") do |req|
98
+ response = connection.put(url) do |req|
161
99
  req.headers = build_headers(auth_token)
162
100
  req.body = submission_obj.to_json
163
101
  req.options.timeout = timeout if timeout.positive?
@@ -172,14 +110,20 @@ module Fastlane
172
110
  end
173
111
  end
174
112
 
175
- def self.commit_submission(app_id, submission_id, auth_token, timeout = 0)
113
+ def self.commit_submission(app_id, flight_id, submission_id, auth_token, timeout = 0)
176
114
  check_app_id(app_id)
177
115
  check_submission_id(submission_id)
178
116
 
117
+ is_flight = !flight_id.nil? && !flight_id.empty?
118
+ check_flight_id(flight_id) if is_flight
119
+
179
120
  connection = Faraday.new(HOST)
121
+ url = build_url_root(app_id)
122
+ url += "/flights/#{flight_id}" if is_flight
123
+ url += "/submissions/#{submission_id}/commit"
180
124
 
181
125
  begin
182
- response = connection.post("/#{API_VERSION}/#{API_ROOT}/#{app_id}/submissions/#{submission_id}/commit") do |req|
126
+ response = connection.post(url) do |req|
183
127
  req.headers = build_headers(auth_token)
184
128
  req.options.timeout = timeout if timeout.positive?
185
129
  end
@@ -195,9 +139,10 @@ module Fastlane
195
139
  check_submission_id(submission_id)
196
140
 
197
141
  connection = Faraday.new(HOST)
142
+ url = "#{build_url_root(app_id)}/submissions/#{submission_id}"
198
143
 
199
144
  begin
200
- response = connection.delete("/#{API_VERSION}/#{API_ROOT}/#{app_id}/submissions/#{submission_id}") do |req|
145
+ response = connection.delete(url) do |req|
201
146
  req.headers = build_headers(auth_token)
202
147
  req.options.timeout = timeout if timeout.positive?
203
148
  end
@@ -208,11 +153,12 @@ module Fastlane
208
153
  end
209
154
  end
210
155
 
211
- def self.get_submission_status(app_id, submission_id, auth_token, timeout = 0)
156
+ def self.get_submission_status(app_id, flight_id, submission_id, auth_token, timeout = 0)
212
157
  check_app_id(app_id)
158
+ check_flight_id(flight_id) if !flight_id.nil? && !flight_id.empty?
213
159
  check_submission_id(submission_id)
214
160
 
215
- response = get_submission_status_internal(app_id, submission_id, auth_token, timeout)
161
+ response = get_submission_status_internal(app_id, flight_id, submission_id, auth_token, timeout)
216
162
 
217
163
  # Sometimes MS can return internal server error code (500) that is not directly related to uploading process.
218
164
  # Once it happens, retry 3 times until we'll get a success response.
@@ -221,7 +167,7 @@ module Fastlane
221
167
 
222
168
  until server_error_500_retry_counter < 2
223
169
  server_error_500_retry_counter += 1
224
- response = get_submission_status_internal(app_id, submission_id, auth_token, timeout)
170
+ response = get_submission_status_internal(app_id, flight_id, submission_id, auth_token, timeout)
225
171
  break if response.nil? || response[:status] == 200
226
172
  end
227
173
  end
@@ -231,11 +177,14 @@ module Fastlane
231
177
  UI.user_error!("Submission status obtaining request returned the error.\nCode: #{response[:status]}")
232
178
  end
233
179
 
234
- def self.get_submission_status_internal(app_id, submission_id, auth_token, timeout = 0)
180
+ def self.get_submission_status_internal(app_id, flight_id, submission_id, auth_token, timeout = 0)
235
181
  connection = Faraday.new(HOST)
182
+ url = build_url_root(app_id)
183
+ url += "/flights/#{flight_id}/" if !flight_id.nil? && !flight_id.empty?
184
+ url += "/submissions/#{submission_id}/status"
236
185
 
237
186
  begin
238
- response = connection.get("/#{API_VERSION}/#{API_ROOT}/#{app_id}/submissions/#{submission_id}/status") do |req|
187
+ response = connection.get(url) do |req|
239
188
  req.headers = build_headers(auth_token)
240
189
  req.options.timeout = timeout if timeout.positive?
241
190
  end
@@ -251,6 +200,36 @@ module Fastlane
251
200
  end
252
201
  end
253
202
 
203
+ def self.create_flight(app_id, friendly_name, group_ids, auth_token, timeout = 0)
204
+ check_app_id(app_id)
205
+
206
+ friendly_name = !friendly_name.nil? && !friendly_name.empty? ? friendly_name : "Fastlane Sapfire Flight"
207
+ body = {
208
+ friendlyName: friendly_name,
209
+ groupIds: group_ids
210
+ }
211
+ connection = Faraday.new(HOST)
212
+ url = "#{build_url_root(app_id)}/flights"
213
+
214
+ begin
215
+ response = connection.post(url) do |req|
216
+ req.headers = build_headers(auth_token)
217
+ req.body = body.to_json
218
+ req.options.timeout = timeout if timeout.positive?
219
+ end
220
+ data = JSON.parse(response.body)
221
+
222
+ return data if response.status == 201
223
+
224
+ code = data["code"]
225
+ message = data["message"]
226
+
227
+ UI.user_error!("Creating flight request returned the error.\nCode: #{response.status} #{code}.\nDescription: #{message}")
228
+ rescue StandardError => ex
229
+ UI.user_error!("Creating flight process failed: #{ex}")
230
+ end
231
+ end
232
+
254
233
  def self.acquire_authorization_token(tenant_id, client_id, client_secret, timeout = 0)
255
234
  body = {
256
235
  client_id: client_id,
@@ -282,10 +261,8 @@ module Fastlane
282
261
  end
283
262
  end
284
263
 
285
- def self.non_published_submission(app_id, auth_token, timeout = 0)
286
- check_app_id(app_id)
287
- app_info = get_app_info(app_id, auth_token, timeout)
288
- app_info["pendingApplicationSubmission"]
264
+ def self.build_url_root(app_id)
265
+ "/#{API_VERSION}/#{API_ROOT}/#{app_id}"
289
266
  end
290
267
 
291
268
  def self.build_headers(auth_token)
@@ -295,21 +272,6 @@ module Fastlane
295
272
  }.merge(REQUEST_HEADERS)
296
273
  end
297
274
 
298
- def self.parse_upload_url(url)
299
- url = URI.parse(url)
300
- query_parts = {}
301
- url.query.split("&").each do |x|
302
- parts = x.split("=")
303
- query_parts[parts[0]] = CGI.unescape(parts[1])
304
- end
305
-
306
- {
307
- host: "https://#{url.host}",
308
- path: url.path,
309
- query: query_parts
310
- }
311
- end
312
-
313
275
  def self.check_app_id(id)
314
276
  UI.user_error!("App ID need to be provided") if !id.is_a?(String) || id.nil? || id.empty?
315
277
  end
@@ -318,30 +280,32 @@ module Fastlane
318
280
  UI.user_error!("Submission ID need to be provided") if !id.is_a?(String) || id.nil? || id.empty?
319
281
  end
320
282
 
321
- public_class_method(:upload_blob)
283
+ def self.check_flight_id(id)
284
+ UI.user_error!("Flight ID need to be provided") if !id.is_a?(String) || id.nil? || id.empty?
285
+ end
286
+
287
+
322
288
  public_class_method(:get_app_info)
289
+ public_class_method(:get_submission)
323
290
  public_class_method(:create_submission)
324
291
  public_class_method(:update_submission)
325
292
  public_class_method(:commit_submission)
326
293
  public_class_method(:remove_submission)
327
294
  public_class_method(:get_submission_status)
295
+ public_class_method(:create_flight)
328
296
  public_class_method(:acquire_authorization_token)
329
- public_class_method(:non_published_submission)
330
297
 
331
- private_class_method(:upload_block)
332
- private_class_method(:upload_block_list)
298
+ private_class_method(:build_url_root)
333
299
  private_class_method(:build_headers)
334
- private_class_method(:parse_upload_url)
335
300
  private_class_method(:check_app_id)
336
301
  private_class_method(:check_submission_id)
302
+ private_class_method(:check_flight_id)
337
303
  private_class_method(:get_submission_status_internal)
338
304
 
339
305
  private_constant(:HOST)
340
306
  private_constant(:API_VERSION)
341
307
  private_constant(:API_ROOT)
342
308
  private_constant(:REQUEST_HEADERS)
343
- private_constant(:FILE_CHUNK_SIZE)
344
- private_constant(:UPLOAD_RETRIES)
345
309
  end
346
310
  end
347
311
  end
@@ -1,5 +1,5 @@
1
1
  module Fastlane
2
2
  module Sapfire
3
- VERSION = "1.2.2"
3
+ VERSION = "1.3.0"
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fastlane-plugin-sapfire
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.2
4
+ version: 1.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - CheeryLee
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-11-27 00:00:00.000000000 Z
11
+ date: 2025-06-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday
@@ -144,7 +144,9 @@ files:
144
144
  - lib/fastlane/plugin/sapfire/actions/update_uwp_signing_settings_action.rb
145
145
  - lib/fastlane/plugin/sapfire/actions/upload_ms_store_action.rb
146
146
  - lib/fastlane/plugin/sapfire/actions/upload_nuget_action.rb
147
+ - lib/fastlane/plugin/sapfire/actions/upload_package_flight_action.rb
147
148
  - lib/fastlane/plugin/sapfire/actions_base/msbuild_action_base.rb
149
+ - lib/fastlane/plugin/sapfire/helper/azure_blob_helper.rb
148
150
  - lib/fastlane/plugin/sapfire/helper/ms_credentials.rb
149
151
  - lib/fastlane/plugin/sapfire/helper/ms_devcenter_helper.rb
150
152
  - lib/fastlane/plugin/sapfire/helper/sapfire_helper.rb
@@ -176,7 +178,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
176
178
  - !ruby/object:Gem::Version
177
179
  version: '0'
178
180
  requirements: []
179
- rubygems_version: 3.3.26
181
+ rubygems_version: 3.4.10
180
182
  signing_key:
181
183
  specification_version: 4
182
184
  summary: A bunch of fastlane actions to work with MSBuild, NuGet and Microsoft Store