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 +4 -4
- data/README.md +5 -0
- data/lib/fastlane/plugin/sapfire/actions/associate_ms_store_action.rb +23 -8
- data/lib/fastlane/plugin/sapfire/actions/upload_ms_store_action.rb +8 -20
- data/lib/fastlane/plugin/sapfire/actions/upload_package_flight_action.rb +233 -0
- data/lib/fastlane/plugin/sapfire/helper/azure_blob_helper.rb +143 -0
- data/lib/fastlane/plugin/sapfire/helper/ms_devcenter_helper.rb +92 -128
- data/lib/fastlane/plugin/sapfire/version.rb +1 -1
- metadata +5 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 226d306173179880376ba2856ece97544e8d0fbeb1b19878a1188c77b2ebe753
|
4
|
+
data.tar.gz: ccee066ce7a4f4bad9e64e9b7939615098984439f4f8fbdf2a7cc6b27533d66b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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.
|
131
|
+
UI.success("Developer info was obtained")
|
126
132
|
|
127
133
|
return developer_info
|
128
134
|
end
|
129
135
|
|
130
|
-
UI.user_error!("Request
|
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.
|
169
|
+
UI.success("Application info was obtained")
|
158
170
|
|
159
171
|
return app_info
|
160
172
|
end
|
161
173
|
|
162
|
-
UI.user_error!("Request
|
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.
|
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.
|
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.
|
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
|
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
|
-
|
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::
|
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.
|
14
|
-
|
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
|
-
|
88
|
-
|
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.
|
93
|
-
req.
|
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
|
24
|
+
return data if response.status == 200
|
99
25
|
|
100
|
-
error
|
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.
|
28
|
+
UI.user_error!("Getting app info process failed: #{ex}")
|
104
29
|
end
|
105
|
-
|
106
|
-
false
|
107
30
|
end
|
108
31
|
|
109
|
-
def self.
|
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(
|
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
|
-
|
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
|
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(
|
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(
|
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(
|
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(
|
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(
|
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.
|
286
|
-
|
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
|
-
|
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(:
|
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
|
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.
|
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:
|
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.
|
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
|