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 +5 -5
- data/README.md +29 -6
- data/lib/fastlane/plugin/appcenter/actions/appcenter_fetch_version_number.rb +103 -0
- data/lib/fastlane/plugin/appcenter/actions/appcenter_upload_action.rb +109 -18
- data/lib/fastlane/plugin/appcenter/helper/appcenter_helper.rb +393 -89
- data/lib/fastlane/plugin/appcenter/version.rb +1 -1
- metadata +18 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 63dc35fc59b2bd98c82d21b139557a0ce06fbdd137367391435cee95d119045d
|
4
|
+
data.tar.gz: 2aa31b4b0d0091f35a0953c89ddbe69bfc0ca6d4b05193a3654455ff1bdd27b3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
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
|
39
|
-
|
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['
|
181
|
-
|
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
|
-
|
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
|
-
|
187
|
-
|
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 =
|
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 ||
|
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
|
-
|
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
|
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.
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
175
|
-
|
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("#{
|
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 #{
|
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("#{
|
238
|
+
UI.error("#{log_type} upload aborted")
|
188
239
|
false
|
189
240
|
end
|
190
241
|
end
|
191
242
|
|
192
|
-
#
|
193
|
-
#
|
194
|
-
#
|
195
|
-
def self.
|
196
|
-
connection = self.connection(
|
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
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
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
|
-
|
212
|
-
|
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
|
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
|
-
|
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.
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
347
|
-
|
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(
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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(
|
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
|
-
|
708
|
+
url = "/v0.1/apps/#{owner_name}/#{app_name}/distribution_groups"
|
481
709
|
|
482
|
-
|
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
|
-
|
737
|
+
url = "/v0.1/apps/#{owner_name}/#{app_name}/distribution_groups/#{ERB::Util.url_encode(distribution_group)}/devices/download_devices_list"
|
507
738
|
|
508
|
-
|
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
|
-
|
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
|
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.
|
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:
|
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
|
-
|
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
|