fastlane-plugin-appcenter 1.4.0 → 1.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f420746739b204f6bfd6e9e5dd23f841028009e63ed49adf45107b053eeb24ac
4
- data.tar.gz: '07896d07c221682028e2c59166c8d248a2ede8d953a3da408f734509eb10ae3a'
3
+ metadata.gz: ba75670e59a7a2ba066a788088aeb05d96991bcc1ab693633bb0fca27fc821c5
4
+ data.tar.gz: b11523450e225a2ec2b596d0a4db135cf1dd1a5acefd4a0a0bc0136cf42c8917
5
5
  SHA512:
6
- metadata.gz: 71b84cb05df74504d82d8aaf83626c999f69fdc06481fc072baada8aa29d24fd9e13652359f84a8cca5417b64f3457b5f33dd12d1a769e388e9aa430b0befb7e
7
- data.tar.gz: 6e85fa4da98dfbba7a4147aad34aed090dc1e69224495be770587149dd0efb27747ac5b4ed4b7d1570b24656a610477dab359edbaaedcd6a05fde8d5ef50a452
6
+ metadata.gz: ffd5e3de2a289e28e264a679fb286bb25ecc830a8c36d15c0d044291bc891f9eb17e76e8e4ea76682bf84b19c18db86d475c20b6cdab5f223fd85d7e207b8d85
7
+ data.tar.gz: d8c07796377fb096fa566c35435e208d22de2dbd3bac1e5901fbff0b432ea7a7ecfdf77b60a5726ff30a87865062602544bd4f0f980c5a96dead0dd1d825855f
data/README.md CHANGED
@@ -12,48 +12,107 @@ This project is a [_fastlane_](https://github.com/fastlane/fastlane) plugin. To
12
12
  fastlane add_plugin appcenter
13
13
  ```
14
14
 
15
+ fastlane v2.96.0 or higher is required for all plugin-actions to function properly.
16
+
15
17
  ## About App Center
16
- With [App Center](https://appcenter.ms) you can continuously build, test, release, and monitor your apps. This plugin provides an `appcenter_upload` action which allows you to upload and [release 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.
18
+ With [App Center](https://appcenter.ms) you can continuously build, test, release, and monitor your apps. This plugin provides a set of actions to interact with App Center.
19
+
20
+ `appcenter_fetch_devices` allows you to obtain the list of iOS devices to distribute an app to (useful for automatic provisioning of testers' devices).
21
+
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.
17
23
 
18
24
  ## Usage
19
25
 
20
26
  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.
21
27
 
22
28
  ```ruby
23
- appcenter_upload(
29
+ appcenter_fetch_devices(
24
30
  api_token: "<appcenter token>",
25
31
  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
26
33
  app_name: "<appcenter app name>",
27
- apk: "<path to android build binary>",
34
+ destinations: "*", # Default is 'Collaborators', use '*' for all distribution groups
35
+ devices_file: "devices.txt" # Default. If you customize, the extension must be .txt
36
+ )
37
+ ```
38
+
39
+ ```ruby
40
+ appcenter_upload(
41
+ api_token: "<appcenter token>",
42
+ owner_name: "<appcenter account name of the owner of the app (username or organization URL name)>",
43
+ owner_type: "user", # Default is user - set to organization for appcenter organizations
44
+ app_name: "<appcenter app name (as seen in app URL)>",
45
+ file: "<path to android build binary>",
28
46
  notify_testers: true # Set to false if you don't want to notify testers of your new release (default: `false`)
29
47
  )
30
48
  ```
31
49
 
32
- The action parameters `api_token` and `owner_name` can also be omitted when their values are [set as environment variables](https://docs.fastlane.tools/advanced/#environment-variables). Below a list of all available environment variables:
33
-
34
- - `APPCENTER_API_TOKEN` - API Token for App Center
35
- - `APPCENTER_OWNER_TYPE` - Owner type - `user` or `organization` (default value is `user`)
36
- - `APPCENTER_OWNER_NAME` - Owner name
37
- - `APPCENTER_APP_NAME` - App name. If there is no app with such name, you will be prompted to create one
38
- - `APPCENTER_DISTRIBUTE_APK` - Build release path for android build
39
- - `APPCENTER_DISTRIBUTE_AAB` - Build release path for android app bundle build
40
- - `APPCENTER_DISTRIBUTE_IPA` - Build release path for ios build
41
- - `APPCENTER_DISTRIBUTE_DSYM` - Path to your symbols (app.dSYM.zip) file
42
- - `APPCENTER_DISTRIBUTE_UPLOAD_DSYM_ONLY` - Flag to upload only the dSYM file to App Center
43
- - `APPCENTER_DISTRIBUTE_ANDROID_MAPPING` - Path to your Android mapping.txt file
44
- - `APPCENTER_DISTRIBUTE_UPLOAD_ANDROID_MAPPING_ONLY` - Flag to upload only the mapping file to App Center
45
- - `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 is `Collaborators`.
46
- - `APPCENTER_DISTRIBUTE_DESTINATION_TYPE` - Destination type of distribution destination. `group` and `store` are supported. Default is `group`
47
- - `APPCENTER_DISTRIBUTE_MANDATORY_UPDATE` - Require users to update to this release
48
- - `APPCENTER_DISTRIBUTE_NOTIFY_TESTERS` - Send email notification about release (default: `false`)
49
- - `APPCENTER_DISTRIBUTE_RELEASE_NOTES` - Release notes
50
- - `APPCENTER_DISTRIBUTE_RELEASE_NOTES_CLIPPING` - Clip release notes if its length is more then 5000, `true` by default
51
- - `APPCENTER_DISTRIBUTE_RELEASE_NOTES_LINK` - Additional release notes link
52
- - `APPCENTER_DISTRIBUTE_TIMEOUT` - Sets the request timeout in seconds. Used when uploading builds to App Center.
50
+ ### Help
51
+
52
+ Once installed, information and help for an action can be printed out with this command:
53
+
54
+ ```bash
55
+ fastlane action appcenter_upload # or any action included with this plugin
56
+ ```
57
+
58
+ ### A note on App Name
59
+
60
+ The `app_name` and `owner_name` as set in the Fastfile come from the app's URL in App Center, in the below form:
61
+ ```
62
+ https://appcenter.ms/users/{owner_name}/apps/{app_name}
63
+ ```
64
+ They should not be confused with the displayed name on App Center pages, which is called `app_display_name ` instead.
65
+
66
+ ### Parameters
67
+
68
+ The action parameters `api_token`, `owner_name`, `app_name`, and others can also be omitted when their values are [set as environment variables](https://docs.fastlane.tools/advanced/#environment-variables). By default, `appcenter_upload` will use the same `api_token`, `owner_name`, and `app_name` you used in `appcenter_fetch_devices`.
69
+
70
+ Here is the list of all existing parameters:
71
+
72
+ #### `appcenter_fetch_devices`
73
+
74
+ | Key & Env Var | Description |
75
+ |-----------------|--------------------|
76
+ | `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
+ | `owner_name` <br/> `APPCENTER_OWNER_NAME` | Owner name, as found in the App's URL in App Center |
79
+ | `destinations` <br/> `APPCENTER_DISTRIBUTE_DESTINATIONS` | Comma separated list of distribution group names. Default is 'Collaborators', use '*' for all distribution groups |
80
+ | `devices_file` <br/> `FL_REGISTER_DEVICES_FILE` | File to save the devices list to. Same environment variable as _fastlane_'s `register_devices` action |
81
+
82
+ #### `appcenter_upload`
83
+
84
+ | Key & Env Var | Description |
85
+ |-----------------|--------------------|
86
+ | `api_token` <br/> `APPCENTER_API_TOKEN` | API Token for App Center |
87
+ | `owner_type` <br/> `APPCENTER_OWNER_TYPE` | Owner type, either 'user' or 'organization' (default: `user`) |
88
+ | `owner_name` <br/> `APPCENTER_OWNER_NAME` | Owner name as found in the App's URL in App Center |
89
+ | `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
+ | `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 |
92
+ | `app_platform` <br/> `APPCENTER_APP_PLATFORM` | App Platform. Used for new app creation, if app 'app_name' was not found |
93
+ | `file` <br/> `APPCENTER_DISTRIBUTE_FILE` | File path to the release build to publish |
94
+ | `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`) |
95
+ | `dsym` <br/> `APPCENTER_DISTRIBUTE_DSYM` | Path to your symbols file. For iOS provide path to app.dSYM.zip |
96
+ | `upload_dsym_only` <br/> `APPCENTER_DISTRIBUTE_UPLOAD_DSYM_ONLY` | Flag to upload only the dSYM file to App Center (default: `false`) |
97
+ | `mapping` <br/> `APPCENTER_DISTRIBUTE_ANDROID_MAPPING` | Path to your Android mapping.txt |
98
+ | `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`) |
100
+ | `destination_type` <br/> `APPCENTER_DISTRIBUTE_DESTINATION_TYPE` | Destination type of distribution destination. 'group' and 'store' are supported (default: `group`) |
101
+ | `mandatory_update` <br/> `APPCENTER_DISTRIBUTE_MANDATORY_UPDATE` | Require users to update to this release. Ignored if destination type is 'store' (default: `false`) |
102
+ | `notify_testers` <br/> `APPCENTER_DISTRIBUTE_NOTIFY_TESTERS` | Send email notification about release. Ignored if destination type is 'store' (default: `false`) |
103
+ | `release_notes` <br/> `APPCENTER_DISTRIBUTE_RELEASE_NOTES` | Release notes (default: `No changelog given`) |
104
+ | `should_clip` <br/> `APPCENTER_DISTRIBUTE_RELEASE_NOTES_CLIPPING` | Clip release notes if its length is more then 5000, true by default (default: `true`) |
105
+ | `release_notes_link` <br/> `APPCENTER_DISTRIBUTE_RELEASE_NOTES_LINK` | Additional release notes link |
106
+ | `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
+ | `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 |
109
+ | `dsa_signature` <br/> `APPCENTER_DISTRIBUTE_DSA_SIGNATURE` | DSA signature of the macOS or Windows release for Sparkle update feed |
110
+ | `strict` <br/> `APPCENTER_STRICT_MODE` | Strict mode, set to 'true' to fail early in case a potential error was detected |
111
+
53
112
 
54
113
  ## Example
55
114
 
56
- Check out the [example `Fastfile`](fastlane/Fastfile) to see how to use this plugin. Try it by cloning the repo, running `fastlane install_plugins` and `bundle exec fastlane test`.
115
+ Check out this [example `Fastfile`](fastlane/Fastfile) to see how to use the `appcenter_upload` action. Try it by cloning the repo, running `fastlane install_plugins` and `bundle exec fastlane test`.
57
116
 
58
117
  Sample uses `.env` for setting private variables like API token, owner name, .etc. You need to replace it in `Fastfile` by your own values.
59
118
 
@@ -62,6 +121,8 @@ There are three examples in `test` lane:
62
121
  - upload release for ios with all set parameters
63
122
  - upload only dSYM file for ios
64
123
 
124
+ Check out this [example `Fastfile`](fastlane/fetch_devices/Fastfile) for a full example of fetching devices, registering them with Apple, provisioning the devices, and signing an app.
125
+
65
126
  ## Run tests for this plugin
66
127
 
67
128
  To run both the tests, and code style validation, run
@@ -95,6 +156,10 @@ _fastlane_ is the easiest way to automate beta deployments and releases for your
95
156
 
96
157
  This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.
97
158
 
159
+ ## Security
160
+
161
+ Check out [SECURITY.md](SECURITY.md) for any security concern with this project.
162
+
98
163
  ## Contact
99
164
 
100
165
  We're on Twitter as [@vsappcenter](https://www.twitter.com/vsappcenter). Additionally you can reach out to us on the [App Center](https://appcenter.ms/apps) portal by using the blue Intercom button on the bottom right to start a conversation.
@@ -0,0 +1,138 @@
1
+ require 'fastlane/action'
2
+ require 'csv'
3
+ require 'faraday'
4
+
5
+ module Fastlane
6
+ module Actions
7
+ module SharedValues
8
+ APPCENTER_API_TOKEN = :APPCENTER_API_TOKEN
9
+ APPCENTER_OWNER_NAME = :APPCENTER_OWNER_NAME
10
+ APPCENTER_APP_NAME = :APPCENTER_APP_NAME
11
+ APPCENTER_DISTRIBUTE_DESTINATIONS = :APPCENTER_DISTRIBUTE_DESTINATIONS
12
+ end
13
+
14
+ class AppcenterFetchDevicesAction < Action
15
+ def self.run(params)
16
+ api_token = params[:api_token]
17
+ owner_name = params[:owner_name]
18
+ app_name = params[:app_name]
19
+ destinations = params[:destinations]
20
+
21
+ Actions.lane_context[SharedValues::APPCENTER_API_TOKEN] = api_token
22
+ Actions.lane_context[SharedValues::APPCENTER_OWNER_NAME] = owner_name
23
+ Actions.lane_context[SharedValues::APPCENTER_APP_NAME] = app_name
24
+
25
+ group_names = []
26
+ if destinations == '*'
27
+ UI.message("Looking up all distribution groups for #{owner_name}/#{app_name}")
28
+ distribution_groups = Helper::AppcenterHelper.fetch_distribution_groups(
29
+ api_token: api_token,
30
+ owner_name: owner_name,
31
+ app_name: app_name
32
+ )
33
+ UI.abort_with_message!("Failed to list distribution groups for #{owner_name}/#{app_name}") unless distribution_groups
34
+ distribution_groups.each do |group|
35
+ group_names << group['name']
36
+ end
37
+ else
38
+ group_names += destinations.split(',').map(&:strip)
39
+ end
40
+
41
+ Actions.lane_context[SharedValues::APPCENTER_DISTRIBUTE_DESTINATIONS] = group_names.join(',')
42
+
43
+ devices = []
44
+ group_names.each do |group_name|
45
+ group_devices = Helper::AppcenterHelper.fetch_devices(
46
+ api_token: api_token,
47
+ owner_name: owner_name,
48
+ app_name: app_name,
49
+ distribution_group: group_name
50
+ )
51
+ UI.abort_with_message!("Failed to get devices for group '#{group_name}'") unless group_devices
52
+ devices << group_devices
53
+ end
54
+
55
+ self.write_devices(devices: devices, devices_file: params[:devices_file])
56
+ end
57
+
58
+ def self.write_devices(devices:, devices_file:)
59
+ CSV.open(devices_file, 'w',
60
+ write_headers: true,
61
+ headers: ['Device ID', 'Device Name'],
62
+ col_sep: "\t") do |csv|
63
+
64
+ devices.each do |device|
65
+ CSV.parse(device, { col_sep: "\t", headers: true }) do |row|
66
+ csv << row
67
+ end
68
+ end
69
+ end
70
+ end
71
+
72
+ def self.description
73
+ "Fetches a list of devices from App Center to distribute an iOS app to"
74
+ end
75
+
76
+ def self.authors
77
+ ["benkane"]
78
+ end
79
+
80
+ def self.return_value
81
+ "CSV file formatted for multi-device registration with Apple"
82
+ end
83
+
84
+ def self.details
85
+ "List is a tab-delimited CSV file containing every device from specified distribution groups for an app in App Center. " +
86
+ "Especially useful when combined with register_devices and match to automatically register and provision devices with Apple. " +
87
+ "By default, only the Collaborators group will be included, use `destination: '*'` to match all groups."
88
+ end
89
+
90
+ def self.available_options
91
+ [
92
+ FastlaneCore::ConfigItem.new(key: :api_token,
93
+ env_name: "APPCENTER_API_TOKEN",
94
+ sensitive: true,
95
+ description: "API Token for App Center",
96
+ optional: false,
97
+ type: String,
98
+ verify_block: proc do |value|
99
+ UI.user_error!("No API token for App Center given, pass using `api_token: 'token'`") unless value && !value.empty?
100
+ end),
101
+ FastlaneCore::ConfigItem.new(key: :owner_name,
102
+ env_name: "APPCENTER_OWNER_NAME",
103
+ description: "Owner name",
104
+ optional: false,
105
+ type: String,
106
+ verify_block: proc do |value|
107
+ UI.user_error!("No Owner name for App Center given, pass using `owner_name: 'name'`") unless value && !value.empty?
108
+ end),
109
+ FastlaneCore::ConfigItem.new(key: :app_name,
110
+ env_name: "APPCENTER_APP_NAME",
111
+ description: "App name",
112
+ optional: false,
113
+ type: String,
114
+ verify_block: proc do |value|
115
+ UI.user_error!("No App name given, pass using `app_name: 'app name'`") unless value && !value.empty?
116
+ end),
117
+ FastlaneCore::ConfigItem.new(key: :devices_file,
118
+ env_name: "FL_REGISTER_DEVICES_FILE",
119
+ description: "File to save devices to",
120
+ type: String,
121
+ default_value: "devices.txt",
122
+ verify_block: proc do |value|
123
+ UI.important("Important: Devices file is #{value}. If you plan to upload this file to Apple Developer Center, the file must have the .txt extension") unless value && value.end_with?('.txt')
124
+ end),
125
+ FastlaneCore::ConfigItem.new(key: :destinations,
126
+ env_name: "APPCENTER_DISTRIBUTE_DESTINATIONS",
127
+ description: "Comma separated list of distribution group names. Default is 'Collaborators', use '*' for all distribution groups",
128
+ default_value: "Collaborators",
129
+ type: String)
130
+ ]
131
+ end
132
+
133
+ def self.is_supported?(platform)
134
+ [:ios].include?(platform)
135
+ end
136
+ end
137
+ end
138
+ end
@@ -2,6 +2,18 @@ module Fastlane
2
2
  module Actions
3
3
  module Constants
4
4
  MAX_RELEASE_NOTES_LENGTH = 5000
5
+ SUPPORTED_EXTENSIONS = {
6
+ android: %w(.aab .apk),
7
+ ios: %w(.ipa),
8
+ mac: %w(.app .app.zip .dmg .pkg),
9
+ windows: %w(.appx .appxbundle .appxupload .msix .msixbundle .msixupload .zip .msi),
10
+ custom: %w(.zip)
11
+ }
12
+ ALL_SUPPORTED_EXTENSIONS = SUPPORTED_EXTENSIONS.values.flatten.sort!.uniq!
13
+ STORE_ONLY_EXTENSIONS = %w(.aab)
14
+ STORE_SUPPORTED_EXTENSIONS = %w(.aab .apk .ipa)
15
+ VERSION_REQUIRED_EXTENSIONS = %w(.msi .zip)
16
+ FULL_VERSION_REQUIRED_EXTENSIONS = %w(.dmg .pkg)
5
17
  end
6
18
 
7
19
  module SharedValues
@@ -16,7 +28,7 @@ module Fastlane
16
28
  api_token = params[:api_token]
17
29
  owner_name = params[:owner_name]
18
30
  app_name = params[:app_name]
19
- file = params[:ipa]
31
+ file = params[:file] || params[:ipa]
20
32
  dsym = params[:dsym]
21
33
  build_number = params[:build_number]
22
34
  version = params[:version]
@@ -64,7 +76,6 @@ module Fastlane
64
76
  end
65
77
 
66
78
  def self.run_mapping_upload(params)
67
- values = params.values
68
79
  api_token = params[:api_token]
69
80
  owner_name = params[:owner_name]
70
81
  app_name = params[:app_name]
@@ -94,6 +105,7 @@ module Fastlane
94
105
  values = params.values
95
106
  api_token = params[:api_token]
96
107
  owner_name = params[:owner_name]
108
+ owner_type = params[:owner_type]
97
109
  app_name = params[:app_name]
98
110
  destinations = params[:destinations]
99
111
  destination_type = params[:destination_type]
@@ -105,6 +117,7 @@ module Fastlane
105
117
  timeout = params[:timeout]
106
118
  build_number = params[:build_number]
107
119
  version = params[:version]
120
+ dsa_signature = params[:dsa_signature]
108
121
 
109
122
  if release_notes.length >= Constants::MAX_RELEASE_NOTES_LENGTH
110
123
  unless should_clip
@@ -119,39 +132,46 @@ module Fastlane
119
132
  end
120
133
 
121
134
  file = [
135
+ params[:file],
122
136
  params[:ipa],
123
137
  params[:apk],
124
138
  params[:aab],
125
- params[:file]
126
139
  ].detect { |e| !e.to_s.empty? }
127
140
 
128
141
  UI.user_error!("Couldn't find build file at path '#{file}'") unless file && File.exist?(file)
129
142
 
130
143
  file_ext = Helper::AppcenterHelper.file_extname_full(file)
131
144
  if destination_type == "group"
132
- UI.user_error!("Can't distribute #{file_ext} to groups, please use `destination_type: 'store'`") if %w(.aab).include? file_ext
145
+ self.optional_error("Can't distribute #{file_ext} to groups, please use `destination_type: 'store'`") if Constants::STORE_ONLY_EXTENSIONS.include? file_ext
133
146
  else
134
- UI.user_error!("Can't distribute #{file_ext} to stores, please use `destination_type: 'group'`") if %w(.app .app.zip .dmg .pkg).include? file_ext
147
+ self.optional_error("Can't distribute #{file_ext} to stores, please use `destination_type: 'group'`") unless Constants::STORE_SUPPORTED_EXTENSIONS.include? file_ext
148
+ 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 == "*"
135
149
  end
136
150
 
151
+ release_upload_body = nil
137
152
  unless params[:file].to_s.empty?
138
- if %w[.dmg .pkg].include? file_ext
139
- UI.user_error!("Fields `version` and `build_number` must be specified to upload a #{file_ext} file") if build_number.to_s.empty? || version.to_s.empty?
140
- release_upload_body = { build_version: version, build_number: build_number }
153
+ if Constants::FULL_VERSION_REQUIRED_EXTENSIONS.include? file_ext
154
+ self.optional_error("Fields `version` and `build_number` must be specified to upload a #{file_ext} file") if build_number.to_s.empty? || version.to_s.empty?
155
+ elsif Constants::VERSION_REQUIRED_EXTENSIONS.include? file_ext
156
+ self.optional_error("Field `version` must be specified to upload a #{file_ext} file") if version.to_s.empty?
141
157
  else
142
- UI.message("Fields `version` and `build_number` are not required for files of type #{file_ext}, ignored") unless build_number.to_s.empty? && version.to_s.empty?
158
+ self.optional_error("Fields `version` and `build_number` are not supported for files of type #{file_ext}") unless build_number.to_s.empty? && version.to_s.empty?
143
159
  end
160
+
161
+ release_upload_body = { build_version: version } unless version.nil?
162
+ release_upload_body = { build_version: version, build_number: build_number } if !version.nil? && !build_number.nil?
144
163
  end
145
164
 
146
165
  if file_ext == ".app" && File.directory?(file)
147
166
  UI.message("App path is a directory, zipping it before upload")
148
167
  zip_file = file + ".zip"
149
- if File.exists? zip_file
168
+ if File.exist? zip_file
150
169
  override = UI.interactive? ? UI.confirm("File '#{zip_file}' already exists, do you want to override it?") : true
151
170
  UI.abort_with_message!("Not overriding, aborting publishing operation") unless override
152
- UI.message("Deleting zip file: #{zip_file}")
171
+ UI.message("Deleting zip archive: #{zip_file}")
153
172
  File.delete zip_file
154
173
  end
174
+ UI.message("Creating zip archive: #{zip_file}")
155
175
  file = Actions::ZipAction.run(path: file, output_path: zip_file)
156
176
  end
157
177
 
@@ -166,29 +186,51 @@ module Fastlane
166
186
 
167
187
  if uploaded
168
188
  release_id = uploaded['release_id']
169
- UI.message("Release '#{release_id}' committed")
170
-
171
- Helper::AppcenterHelper.update_release(api_token, owner_name, app_name, release_id, release_notes)
172
-
173
- destinations_array = destinations.split(',')
189
+ release_url = Helper::AppcenterHelper.get_release_url(owner_type, owner_name, app_name, release_id)
190
+ UI.message("Release '#{release_id}' committed: #{release_url}")
191
+
192
+ release = Helper::AppcenterHelper.update_release(api_token, owner_name, app_name, release_id, release_notes)
193
+ Helper::AppcenterHelper.update_release_metadata(api_token, owner_name, app_name, release_id, dsa_signature)
194
+
195
+ destinations_array = []
196
+ if destinations == '*'
197
+ UI.message("Looking up all distribution groups for #{owner_name}/#{app_name}")
198
+ distribution_groups = Helper::AppcenterHelper.fetch_distribution_groups(
199
+ api_token: api_token,
200
+ owner_name: owner_name,
201
+ app_name: app_name
202
+ )
203
+
204
+ UI.abort_with_message!("Failed to list distribution groups for #{owner_name}/#{app_name}") unless distribution_groups
205
+
206
+ destinations_array = distribution_groups.map {|h| h['name'] }
207
+ else
208
+ destinations_array = destinations.split(',').map(&:strip)
209
+ end
210
+
174
211
  destinations_array.each do |destination_name|
175
212
  destination = Helper::AppcenterHelper.get_destination(api_token, owner_name, app_name, destination_type, destination_name)
176
213
  if destination
177
214
  destination_id = destination['id']
178
215
  distributed_release = Helper::AppcenterHelper.add_to_destination(api_token, owner_name, app_name, release_id, destination_type, destination_id, mandatory_update, notify_testers)
179
216
  if distributed_release
180
- UI.success("Release #{distributed_release['short_version']} was successfully distributed to #{destination_type} \"#{destination_name}\"")
217
+ UI.success("Release '#{release_id}' (#{distributed_release['short_version']}) was successfully distributed to #{destination_type} \"#{destination_name}\"")
181
218
  else
182
- UI.error("Release '#{release_id}' was not found")
219
+ UI.error("Release '#{release_id}' was not found for destination '#{destination_name}'")
183
220
  end
184
221
  else
185
222
  UI.error("#{destination_type} '#{destination_name}' was not found")
186
223
  end
187
224
  end
225
+
226
+ safe_download_url = Helper::AppcenterHelper.get_install_url(owner_type, owner_name, app_name)
227
+ UI.message("Release '#{release_id}' is available for download at: #{safe_download_url}")
188
228
  else
189
229
  UI.user_error!("Failed to upload release")
190
230
  end
191
231
  end
232
+
233
+ release
192
234
  end
193
235
 
194
236
  # checks app existance, if ther is no such - creates it
@@ -204,11 +246,17 @@ module Fastlane
204
246
  platforms = {
205
247
  Android: %w[Java React-Native Xamarin],
206
248
  iOS: %w[Objective-C-Swift React-Native Xamarin],
207
- macOS: %w[Objective-C-Swift]
249
+ macOS: %w[Objective-C-Swift],
250
+ Windows: %w[UWP WPF WinForms Unity]
208
251
  }
209
252
 
210
- if Helper::AppcenterHelper.get_app(api_token, owner_name, app_name)
211
- return true
253
+ begin
254
+ if Helper::AppcenterHelper.get_app(api_token, owner_name, app_name)
255
+ return true
256
+ end
257
+ rescue URI::InvalidURIError
258
+ UI.user_error!("Provided app_name: '#{app_name}' is not in a valid format. Please ensure no special characters or spaces in the app_name.")
259
+ return false
212
260
  end
213
261
 
214
262
  should_create_app = !app_display_name.to_s.empty? || !app_os.to_s.empty? || !app_platform.to_s.empty?
@@ -216,9 +264,9 @@ module Fastlane
216
264
  if Helper.test? || should_create_app || UI.confirm("App with name #{app_name} not found, create one?")
217
265
  app_display_name = app_name if app_display_name.to_s.empty?
218
266
  os = app_os.to_s.empty? && (Helper.test? ? "Android" : UI.select("Select OS", platforms.keys)) || app_os.to_s
219
- platform = app_platform.to_s.empty? && (Helper.test? ? "Java" : app_platform.to_s) || app_platform.to_s
267
+ platform = app_platform.to_s.empty? && (Helper.test? ? platforms[os.to_sym][0] : app_platform.to_s) || app_platform.to_s
220
268
  if platform.to_s.empty?
221
- platform = platforms[os].length == 1 ? platforms[os][0] : UI.select("Select Platform", platforms[os])
269
+ platform = platforms[os.to_sym].length == 1 ? platforms[os.to_sym][0] : UI.select("Select Platform", platforms[os.to_sym])
222
270
  end
223
271
 
224
272
  Helper::AppcenterHelper.create_app(api_token, owner_type, owner_name, app_name, app_display_name, os, platform)
@@ -230,14 +278,20 @@ module Fastlane
230
278
 
231
279
  def self.run(params)
232
280
  values = params.values
281
+ upload_build_only = params[:upload_build_only]
233
282
  upload_dsym_only = params[:upload_dsym_only]
234
283
  upload_mapping_only = params[:upload_mapping_only]
235
284
 
285
+ Options.strict_mode(params[:strict])
286
+
236
287
  # if app found or successfully created
237
288
  if self.get_or_create_app(params)
238
- self.run_release_upload(params) unless upload_dsym_only || upload_mapping_only
239
- self.run_dsym_upload(params) unless upload_mapping_only
240
- self.run_mapping_upload(params) unless upload_dsym_only
289
+ release = self.run_release_upload(params) unless upload_dsym_only || upload_mapping_only
290
+ params[:version] = release['short_version'] if release
291
+ params[:build_number] = release['version'] if release
292
+
293
+ self.run_dsym_upload(params) unless upload_mapping_only || upload_build_only
294
+ self.run_mapping_upload(params) unless upload_dsym_only || upload_build_only
241
295
  end
242
296
 
243
297
  return values if Helper.test?
@@ -260,6 +314,7 @@ module Fastlane
260
314
  FastlaneCore::ConfigItem.new(key: :api_token,
261
315
  env_name: "APPCENTER_API_TOKEN",
262
316
  description: "API Token for App Center",
317
+ default_value: Actions.lane_context[SharedValues::APPCENTER_API_TOKEN],
263
318
  optional: false,
264
319
  type: String,
265
320
  verify_block: proc do |value|
@@ -268,18 +323,19 @@ module Fastlane
268
323
 
269
324
  FastlaneCore::ConfigItem.new(key: :owner_type,
270
325
  env_name: "APPCENTER_OWNER_TYPE",
271
- description: "Owner type",
326
+ description: "Owner type, either 'user' or 'organization'",
272
327
  optional: true,
273
328
  default_value: "user",
274
329
  type: String,
275
330
  verify_block: proc do |value|
276
331
  accepted_formats = ["user", "organization"]
277
- UI.user_error!("Only \"user\" and \"organization\" types are allowed, you provided \"#{File.extname(value)}\"") unless accepted_formats.include? value
332
+ UI.user_error!("Only \"user\" and \"organization\" types are allowed, you provided \"#{value}\"") unless accepted_formats.include? value
278
333
  end),
279
334
 
280
335
  FastlaneCore::ConfigItem.new(key: :owner_name,
281
336
  env_name: "APPCENTER_OWNER_NAME",
282
- description: "Owner name",
337
+ description: "Owner name as found in the App's URL in App Center",
338
+ default_value: Actions.lane_context[SharedValues::APPCENTER_OWNER_NAME],
283
339
  optional: false,
284
340
  type: String,
285
341
  verify_block: proc do |value|
@@ -288,7 +344,8 @@ module Fastlane
288
344
 
289
345
  FastlaneCore::ConfigItem.new(key: :app_name,
290
346
  env_name: "APPCENTER_APP_NAME",
291
- description: "App name. If there is no app with such name, you will be prompted to create one",
347
+ description: "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",
348
+ default_value: Actions.lane_context[SharedValues::APPCENTER_APP_NAME],
292
349
  optional: false,
293
350
  type: String,
294
351
  verify_block: proc do |value|
@@ -303,13 +360,13 @@ module Fastlane
303
360
 
304
361
  FastlaneCore::ConfigItem.new(key: :app_os,
305
362
  env_name: "APPCENTER_APP_OS",
306
- description: "App OS. Used for new app creation, if app with 'app_name' name was not found",
363
+ description: "App OS. Used for new app creation, if app 'app_name' was not found",
307
364
  optional: true,
308
365
  type: String),
309
366
 
310
367
  FastlaneCore::ConfigItem.new(key: :app_platform,
311
368
  env_name: "APPCENTER_APP_PLATFORM",
312
- description: "App Platform. Used for new app creation, if app with 'app_name' name was not found",
369
+ description: "App Platform. Used for new app creation, if app 'app_name' was not found",
313
370
  optional: true,
314
371
  type: String),
315
372
 
@@ -318,6 +375,7 @@ module Fastlane
318
375
  description: "Build release path for android build",
319
376
  default_value: Actions.lane_context[SharedValues::GRADLE_APK_OUTPUT_PATH],
320
377
  optional: true,
378
+ deprecated: true,
321
379
  type: String,
322
380
  conflicting_options: [:ipa, :aab, :file],
323
381
  conflict_block: proc do |value|
@@ -326,7 +384,7 @@ module Fastlane
326
384
  verify_block: proc do |value|
327
385
  accepted_formats = [".apk"]
328
386
  file_extname_full = Helper::AppcenterHelper.file_extname_full(value)
329
- UI.user_error!("Only \".apk\" formats are allowed, you provided \"#{file_extname_full}\"") unless accepted_formats.include? file_extname_full
387
+ self.optional_error("Only \".apk\" formats are allowed, you provided \"#{file_extname_full}\"") unless accepted_formats.include? file_extname_full
330
388
  end),
331
389
 
332
390
  FastlaneCore::ConfigItem.new(key: :aab,
@@ -334,6 +392,7 @@ module Fastlane
334
392
  description: "Build release path for android app bundle build",
335
393
  default_value: Actions.lane_context[SharedValues::GRADLE_AAB_OUTPUT_PATH],
336
394
  optional: true,
395
+ deprecated: true,
337
396
  type: String,
338
397
  conflicting_options: [:ipa, :apk, :file],
339
398
  conflict_block: proc do |value|
@@ -341,7 +400,7 @@ module Fastlane
341
400
  end,
342
401
  verify_block: proc do |value|
343
402
  accepted_formats = [".aab"]
344
- UI.user_error!("Only \".aab\" formats are allowed, you provided \"#{File.extname(value)}\"") unless accepted_formats.include? File.extname(value)
403
+ self.optional_error("Only \".aab\" formats are allowed, you provided \"#{File.extname(value)}\"") unless accepted_formats.include? File.extname(value)
345
404
  end),
346
405
 
347
406
  FastlaneCore::ConfigItem.new(key: :ipa,
@@ -349,6 +408,7 @@ module Fastlane
349
408
  description: "Build release path for iOS builds",
350
409
  default_value: Actions.lane_context[SharedValues::IPA_OUTPUT_PATH],
351
410
  optional: true,
411
+ deprecated: true,
352
412
  type: String,
353
413
  conflicting_options: [:apk, :aab, :file],
354
414
  conflict_block: proc do |value|
@@ -356,12 +416,12 @@ module Fastlane
356
416
  end,
357
417
  verify_block: proc do |value|
358
418
  accepted_formats = [".ipa"]
359
- UI.user_error!("Only \".ipa\" formats are allowed, you provided \"#{File.extname(value)}\"") unless accepted_formats.include? File.extname(value)
419
+ self.optional_error("Only \".ipa\" formats are allowed, you provided \"#{File.extname(value)}\"") unless accepted_formats.include? File.extname(value)
360
420
  end),
361
421
 
362
422
  FastlaneCore::ConfigItem.new(key: :file,
363
423
  env_name: "APPCENTER_DISTRIBUTE_FILE",
364
- description: "Build release path for generic builds (.aab, .app, .app.zip, .apk, .dmg, .ipa, .pkg)",
424
+ description: "File path to the release build to publish",
365
425
  optional: true,
366
426
  type: String,
367
427
  conflicting_options: [:apk, :aab, :ipa],
@@ -369,11 +429,29 @@ module Fastlane
369
429
  UI.user_error!("You can't use 'file' and '#{value.key}' options in one run")
370
430
  end,
371
431
  verify_block: proc do |value|
372
- accepted_formats = %w(.aab .app .app.zip .apk .dmg .ipa .pkg)
373
- file_ext = Helper::AppcenterHelper.file_extname_full(value)
374
- UI.user_error!("Only #{accepted_formats.to_s} formats are allowed, you provided \"#{file_ext}\"") unless accepted_formats.include? file_ext
432
+ platform = Actions.lane_context[SharedValues::PLATFORM_NAME]
433
+ if platform
434
+ accepted_formats = Constants::SUPPORTED_EXTENSIONS[platform.to_sym]
435
+ unless accepted_formats
436
+ UI.important("Unknown platform '#{platform}', Supported are #{Constants::SUPPORTED_EXTENSIONS.keys}")
437
+ accepted_formats = Constants::ALL_SUPPORTED_EXTENSIONS
438
+ end
439
+ file_ext = Helper::AppcenterHelper.file_extname_full(value)
440
+ self.optional_error("Extension not supported: '#{file_ext}'. Supported formats for platform '#{platform}': #{accepted_formats.join ' '}") unless accepted_formats.include? file_ext
441
+ end
375
442
  end),
376
443
 
444
+ FastlaneCore::ConfigItem.new(key: :upload_build_only,
445
+ env_name: "APPCENTER_DISTRIBUTE_UPLOAD_BUILD_ONLY",
446
+ description: "Flag to upload only the build to App Center. Skips uploading symbols or mapping",
447
+ optional: true,
448
+ is_string: false,
449
+ default_value: false,
450
+ conflicting_options: [:upload_dsym_only, :upload_mapping_only],
451
+ conflict_block: proc do |value|
452
+ UI.user_error!("You can't use 'upload_build_only' and '#{value.key}' options in one run")
453
+ end),
454
+
377
455
  FastlaneCore::ConfigItem.new(key: :dsym,
378
456
  env_name: "APPCENTER_DISTRIBUTE_DSYM",
379
457
  description: "Path to your symbols file. For iOS provide path to app.dSYM.zip",
@@ -398,6 +476,7 @@ module Fastlane
398
476
  FastlaneCore::ConfigItem.new(key: :mapping,
399
477
  env_name: "APPCENTER_DISTRIBUTE_ANDROID_MAPPING",
400
478
  description: "Path to your Android mapping.txt",
479
+ default_value: (defined? SharedValues::GRADLE_MAPPING_TXT_OUTPUT_PATH) && Actions.lane_context[SharedValues::GRADLE_MAPPING_TXT_OUTPUT_PATH] || nil,
401
480
  optional: true,
402
481
  type: String,
403
482
  verify_block: proc do |value|
@@ -427,12 +506,11 @@ module Fastlane
427
506
 
428
507
  FastlaneCore::ConfigItem.new(key: :destinations,
429
508
  env_name: "APPCENTER_DISTRIBUTE_DESTINATIONS",
430
- 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",
431
- default_value: "Collaborators",
509
+ 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",
510
+ default_value: Actions.lane_context[SharedValues::APPCENTER_DISTRIBUTE_DESTINATIONS] || "Collaborators",
432
511
  optional: true,
433
512
  type: String),
434
513
 
435
-
436
514
  FastlaneCore::ConfigItem.new(key: :destination_type,
437
515
  env_name: "APPCENTER_DISTRIBUTE_DESTINATION_TYPE",
438
516
  description: "Destination type of distribution destination. 'group' and 'store' are supported",
@@ -479,13 +557,13 @@ module Fastlane
479
557
 
480
558
  FastlaneCore::ConfigItem.new(key: :build_number,
481
559
  env_name: "APPCENTER_DISTRIBUTE_BUILD_NUMBER",
482
- description: "The build number, required for Android ProGuard mapping files, as well as macOS .pkg and .dmg builds",
560
+ description: "The build number, required for macOS .pkg and .dmg builds, as well as Android ProGuard `mapping.txt` when using `upload_mapping_only`",
483
561
  optional: true,
484
562
  type: String),
485
563
 
486
564
  FastlaneCore::ConfigItem.new(key: :version,
487
565
  env_name: "APPCENTER_DISTRIBUTE_VERSION",
488
- description: "The build version, required for Android ProGuard mapping files, as well as macOS .pkg and .dmg builds",
566
+ description: "The build version, required for .pkg, .dmg, .zip and .msi builds, as well as Android ProGuard `mapping.txt` when using `upload_mapping_only`",
489
567
  optional: true,
490
568
  type: String),
491
569
 
@@ -494,6 +572,18 @@ module Fastlane
494
572
  description: "Request timeout in seconds",
495
573
  optional: true,
496
574
  type: Integer),
575
+
576
+ FastlaneCore::ConfigItem.new(key: :dsa_signature,
577
+ env_name: "APPCENTER_DISTRIBUTE_DSA_SIGNATURE",
578
+ description: "DSA signature of the macOS or Windows release for Sparkle update feed",
579
+ optional: true,
580
+ type: String),
581
+
582
+ FastlaneCore::ConfigItem.new(key: :strict,
583
+ env_name: "APPCENTER_STRICT_MODE",
584
+ description: "Strict mode, set to 'true' to fail early in case a potential error was detected",
585
+ optional: true,
586
+ type: String)
497
587
  ]
498
588
  end
499
589
 
@@ -505,6 +595,8 @@ module Fastlane
505
595
  end
506
596
 
507
597
  def self.is_supported?(platform)
598
+ return Constants::SUPPORTED_EXTENSIONS.keys.include?(platform) if Options.strict
599
+
508
600
  true
509
601
  end
510
602
 
@@ -513,12 +605,10 @@ module Fastlane
513
605
  'appcenter_upload(
514
606
  api_token: "...",
515
607
  owner_name: "appcenter_owner",
516
- app_name: "testing_app",
517
- apk: "./app-release.apk",
608
+ app_name: "testing_android_app",
609
+ file: "./app-release.apk",
518
610
  destinations: "Testers",
519
611
  destination_type: "group",
520
- build_number: "3",
521
- version: "1.0.0",
522
612
  mapping: "./mapping.txt",
523
613
  release_notes: "release notes",
524
614
  notify_testers: false
@@ -526,9 +616,9 @@ module Fastlane
526
616
  'appcenter_upload(
527
617
  api_token: "...",
528
618
  owner_name: "appcenter_owner",
529
- app_name: "testing_app",
530
- apk: "./app-release.ipa",
531
- destinations: "Testers,Alpha",
619
+ app_name: "testing_ios_app",
620
+ file: "./app-release.ipa",
621
+ destinations: "Testers,Public",
532
622
  destination_type: "group",
533
623
  dsym: "./app.dSYM.zip",
534
624
  release_notes: "release notes",
@@ -537,18 +627,46 @@ module Fastlane
537
627
  'appcenter_upload(
538
628
  api_token: "...",
539
629
  owner_name: "appcenter_owner",
540
- app_name: "testing_app",
541
- aab: "./app.aab",
542
- destinations: "Alpha",
543
- destination_type: "store",
544
- build_number: "3",
545
- version: "1.0.0",
546
- mapping: "./mapping.txt",
630
+ app_name: "testing_ios_app",
631
+ file: "./app-release.ipa",
632
+ destinations: "*",
633
+ destination_type: "group",
547
634
  release_notes: "release notes",
548
635
  notify_testers: false
636
+ )',
637
+ 'appcenter_upload(
638
+ api_token: "...",
639
+ owner_name: "appcenter_owner",
640
+ app_name: "testing_google_play_app",
641
+ file: "./app.aab",
642
+ destinations: "Alpha",
643
+ destination_type: "store",
644
+ release_notes: "this is a store release"
549
645
  )'
550
646
  ]
551
647
  end
648
+
649
+ class Options
650
+ include Singleton
651
+
652
+ def self.strict_mode(mode)
653
+ @strict = mode.to_s == "true"
654
+ UI.message("Enabled strict mode") if @strict
655
+ end
656
+
657
+ def self.strict
658
+ @strict
659
+ end
660
+ end
661
+
662
+ def self.optional_error(message)
663
+ if Options.strict
664
+ UI.user_error!(message)
665
+ else
666
+ UI.important(message)
667
+ UI.important("The current operation might fail, trying anyway...")
668
+ end
669
+ end
552
670
  end
553
671
  end
554
672
  end
@@ -6,22 +6,20 @@ module Fastlane
6
6
  # accounting for file types that can and should be zip-compressed
7
7
  # before they are uploaded
8
8
  def self.file_extname_full(path)
9
- is_zip = File.extname(path) == ".zip"
10
-
11
- # if file is not .zip'ed, these do not change basename and extname
12
- unzip_basename = File.basename(path, ".zip")
13
- unzip_extname = File.extname(unzip_basename)
9
+ %w(.app.zip .dSYM.zip).each do |suffix|
10
+ return suffix if path.to_s.downcase.end_with? suffix.downcase
11
+ end
14
12
 
15
- is_zip ? unzip_extname + ".zip" : unzip_extname
13
+ File.extname path
16
14
  end
17
15
 
18
16
  # create request
19
- def self.connection(upload_url = false, dsym = false)
17
+ def self.connection(upload_url = nil, dsym = false, csv = false)
20
18
  require 'faraday'
21
19
  require 'faraday_middleware'
22
20
 
23
21
  options = {
24
- url: upload_url ? upload_url : ENV.fetch('APPCENTER_UPLOAD_URL', "https://api.appcenter.ms")
22
+ url: upload_url || ENV.fetch('APPCENTER_UPLOAD_URL', "https://api.appcenter.ms")
25
23
  }
26
24
 
27
25
  Faraday.new(options) do |builder|
@@ -31,7 +29,7 @@ module Fastlane
31
29
  else
32
30
  builder.request :json
33
31
  end
34
- builder.response :json, content_type: /\bjson$/
32
+ builder.response :json, content_type: /\bjson$/ unless csv
35
33
  builder.use FaradayMiddleware::FollowRedirects
36
34
  builder.adapter :net_http
37
35
  end
@@ -151,6 +149,9 @@ module Fastlane
151
149
  when 200...300
152
150
  UI.message("DEBUG: #{JSON.pretty_generate(response.body)}\n") if ENV['DEBUG']
153
151
  response.body
152
+ when 401
153
+ UI.user_error!("Auth Error, provided invalid token")
154
+ false
154
155
  else
155
156
  UI.error("Error #{response.status}: #{response.body}")
156
157
  false
@@ -177,6 +178,9 @@ module Fastlane
177
178
  when 200...300
178
179
  self.update_symbol_upload(api_token, owner_name, app_name, symbol_upload_id, 'committed')
179
180
  UI.success("#{logType} uploaded")
181
+ when 401
182
+ UI.user_error!("Auth Error, provided invalid token")
183
+ false
180
184
  else
181
185
  UI.error("Error uploading #{logType} #{response.status}: #{response.body}")
182
186
  self.update_symbol_upload(api_token, owner_name, app_name, symbol_upload_id, 'aborted')
@@ -206,6 +210,9 @@ module Fastlane
206
210
  when 200...300
207
211
  UI.message("Binary uploaded")
208
212
  self.update_release_upload(api_token, owner_name, app_name, upload_id, 'committed')
213
+ when 401
214
+ UI.user_error!("Auth Error, provided invalid token")
215
+ false
209
216
  else
210
217
  UI.error("Error uploading binary #{response.status}: #{response.body}")
211
218
  self.update_release_upload(api_token, owner_name, app_name, upload_id, 'aborted')
@@ -230,6 +237,9 @@ module Fastlane
230
237
  when 200...300
231
238
  UI.message("DEBUG: #{JSON.pretty_generate(response.body)}\n") if ENV['DEBUG']
232
239
  response.body
240
+ when 401
241
+ UI.user_error!("Auth Error, provided invalid token")
242
+ false
233
243
  when 500...600
234
244
  UI.crash!("Internal Service Error, please try again later")
235
245
  else
@@ -254,6 +264,9 @@ module Fastlane
254
264
  when 404
255
265
  UI.error("Not found, invalid release url")
256
266
  false
267
+ when 401
268
+ UI.user_error!("Auth Error, provided invalid token")
269
+ false
257
270
  else
258
271
  UI.error("Error fetching information about release #{response.status}: #{response.body}")
259
272
  false
@@ -277,6 +290,9 @@ module Fastlane
277
290
  when 404
278
291
  UI.error("Not found, invalid distribution #{destination_type} name")
279
292
  false
293
+ when 401
294
+ UI.user_error!("Auth Error, provided invalid token")
295
+ false
280
296
  else
281
297
  UI.error("Error getting #{destination_type} #{response.status}: #{response.body}")
282
298
  false
@@ -291,7 +307,7 @@ module Fastlane
291
307
  req.headers['X-API-Token'] = api_token
292
308
  req.headers['internal-request-source'] = "fastlane"
293
309
  req.body = {
294
- "release_notes" => release_notes
310
+ release_notes: release_notes
295
311
  }
296
312
  end
297
313
 
@@ -300,6 +316,7 @@ module Fastlane
300
316
  # get full release info
301
317
  release = self.get_release(api_token, owner_name, app_name, release_id)
302
318
  return false unless release
319
+
303
320
  download_url = release['download_url']
304
321
 
305
322
  UI.message("DEBUG: #{JSON.pretty_generate(release)}") if ENV['DEBUG']
@@ -307,18 +324,53 @@ module Fastlane
307
324
  Actions.lane_context[Fastlane::Actions::SharedValues::APPCENTER_DOWNLOAD_LINK] = download_url
308
325
  Actions.lane_context[Fastlane::Actions::SharedValues::APPCENTER_BUILD_INFORMATION] = release
309
326
 
310
- UI.message("Release #{release['short_version']} was successfully updated")
327
+ UI.message("Release '#{release_id}' (#{release['short_version']}) was successfully updated")
311
328
 
312
329
  release
313
330
  when 404
314
331
  UI.error("Not found, invalid release id")
315
332
  false
333
+ when 401
334
+ UI.user_error!("Auth Error, provided invalid token")
335
+ false
316
336
  else
317
337
  UI.error("Error adding updating release #{response.status}: #{response.body}")
318
338
  false
319
339
  end
320
340
  end
321
341
 
342
+ # updates release metadata
343
+ def self.update_release_metadata(api_token, owner_name, app_name, release_id, dsa_signature)
344
+ return if dsa_signature.to_s == ''
345
+
346
+ release_metadata = {
347
+ dsa_signature: dsa_signature
348
+ }
349
+
350
+ connection = self.connection
351
+ response = connection.patch("v0.1/apps/#{owner_name}/#{app_name}/releases/#{release_id}") do |req|
352
+ req.headers['X-API-Token'] = api_token
353
+ req.headers['internal-request-source'] = "fastlane"
354
+ req.body = {
355
+ metadata: release_metadata
356
+ }
357
+ end
358
+
359
+ case response.status
360
+ when 200...300
361
+ UI.message("Release Metadata was successfully updated for release '#{release_id}'")
362
+ when 404
363
+ UI.error("Not found, invalid release id")
364
+ false
365
+ when 401
366
+ UI.user_error!("Auth Error, provided invalid token")
367
+ false
368
+ else
369
+ UI.error("Error adding updating release metadata #{response.status}: #{response.body}")
370
+ false
371
+ end
372
+ end
373
+
322
374
  # add release to distribution group or store
323
375
  def self.add_to_destination(api_token, owner_name, app_name, release_id, destination_type, destination_id, mandatory_update = false, notify_testers = false)
324
376
  connection = self.connection
@@ -342,6 +394,7 @@ module Fastlane
342
394
  # get full release info
343
395
  release = self.get_release(api_token, owner_name, app_name, release_id)
344
396
  return false unless release
397
+
345
398
  download_url = release['download_url']
346
399
 
347
400
  UI.message("DEBUG: received release #{JSON.pretty_generate(release)}") if ENV['DEBUG']
@@ -349,12 +402,15 @@ module Fastlane
349
402
  Actions.lane_context[Fastlane::Actions::SharedValues::APPCENTER_DOWNLOAD_LINK] = download_url
350
403
  Actions.lane_context[Fastlane::Actions::SharedValues::APPCENTER_BUILD_INFORMATION] = release
351
404
 
352
- UI.message("Public Download URL: #{download_url}") if download_url
405
+ UI.message("Release '#{release_id}' (#{release['short_version']}) was successfully distributed'")
353
406
 
354
407
  release
355
408
  when 404
356
409
  UI.error("Not found, invalid distribution #{destination_type} name")
357
410
  false
411
+ when 401
412
+ UI.user_error!("Auth Error, provided invalid token")
413
+ false
358
414
  else
359
415
  UI.error("Error adding to #{destination_type} #{response.status}: #{response.body}")
360
416
  false
@@ -377,6 +433,9 @@ module Fastlane
377
433
  when 404
378
434
  UI.message("DEBUG: #{JSON.pretty_generate(response.body)}\n") if ENV['DEBUG']
379
435
  false
436
+ when 401
437
+ UI.user_error!("Auth Error, provided invalid token")
438
+ false
380
439
  else
381
440
  UI.error("Error getting app #{owner_name}/#{app_name}, #{response.status}: #{response.body}")
382
441
  false
@@ -406,11 +465,78 @@ module Fastlane
406
465
  UI.message("DEBUG: #{JSON.pretty_generate(created)}") if ENV['DEBUG']
407
466
  UI.success("Created #{os}/#{platform} app with name \"#{created['name']}\" and display name \"#{created['display_name']}\" for #{owner_type} \"#{owner_name}\"")
408
467
  true
468
+ when 401
469
+ UI.user_error!("Auth Error, provided invalid token")
470
+ false
409
471
  else
410
472
  UI.error("Error creating app #{response.status}: #{response.body}")
411
473
  false
412
474
  end
413
475
  end
476
+
477
+ def self.fetch_distribution_groups(api_token:, owner_name:, app_name:)
478
+ connection = self.connection
479
+
480
+ endpoint = "/v0.1/apps/#{owner_name}/#{app_name}/distribution_groups"
481
+
482
+ response = connection.get(endpoint) do |req|
483
+ req.headers['X-API-Token'] = api_token
484
+ req.headers['internal-request-source'] = "fastlane"
485
+ end
486
+
487
+ case response.status
488
+ when 200...300
489
+ UI.message("DEBUG: #{JSON.pretty_generate(response.body)}\n") if ENV['DEBUG']
490
+ response.body
491
+ when 401
492
+ UI.user_error!("Auth Error, provided invalid token")
493
+ false
494
+ when 404
495
+ UI.error("Not found, invalid owner or application name")
496
+ false
497
+ else
498
+ UI.error("Error #{response.status}: #{response.body}")
499
+ false
500
+ end
501
+ end
502
+
503
+ def self.fetch_devices(api_token:, owner_name:, app_name:, distribution_group:)
504
+ connection = self.connection(nil, false, true)
505
+
506
+ endpoint = "/v0.1/apps/#{owner_name}/#{app_name}/distribution_groups/#{ERB::Util.url_encode(distribution_group)}/devices/download_devices_list"
507
+
508
+ response = connection.get(endpoint) do |req|
509
+ req.headers['X-API-Token'] = api_token
510
+ req.headers['internal-request-source'] = "fastlane"
511
+ end
512
+
513
+ case response.status
514
+ when 200...300
515
+ UI.message("DEBUG: #{response.body.inspect}") if ENV['DEBUG']
516
+ response.body
517
+ when 401
518
+ UI.user_error!("Auth Error, provided invalid token")
519
+ false
520
+ when 404
521
+ UI.error("Not found, invalid owner, application or distribution group name")
522
+ false
523
+ else
524
+ UI.error("Error #{response.status}: #{response.body}")
525
+ false
526
+ end
527
+ end
528
+
529
+ # Note: This does not support testing environment (INT)
530
+ def self.get_release_url(owner_type, owner_name, app_name, release_id)
531
+ owner_path = owner_type == "user" ? "users/#{owner_name}" : "orgs/#{owner_name}"
532
+ return "https://appcenter.ms/#{owner_path}/apps/#{app_name}/distribute/releases/#{release_id}"
533
+ end
534
+
535
+ # Note: This does not support testing environment (INT)
536
+ def self.get_install_url(owner_type, owner_name, app_name)
537
+ owner_path = owner_type == "user" ? "users/#{owner_name}" : "orgs/#{owner_name}"
538
+ return "https://install.appcenter.ms/#{owner_path}/apps/#{app_name}"
539
+ end
414
540
  end
415
541
  end
416
542
  end
@@ -1,5 +1,5 @@
1
1
  module Fastlane
2
2
  module Appcenter
3
- VERSION = "1.4.0"
3
+ VERSION = "1.7.1"
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fastlane-plugin-appcenter
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.4.0
4
+ version: 1.7.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Microsoft Corporation
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-08-30 00:00:00.000000000 Z
11
+ date: 2019-12-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -30,14 +30,14 @@ dependencies:
30
30
  requirements:
31
31
  - - ">="
32
32
  - !ruby/object:Gem::Version
33
- version: 2.29.0
33
+ version: 2.96.0
34
34
  type: :development
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - ">="
39
39
  - !ruby/object:Gem::Version
40
- version: 2.29.0
40
+ version: 2.96.0
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: pry
43
43
  requirement: !ruby/object:Gem::Requirement
@@ -81,7 +81,7 @@ dependencies:
81
81
  - !ruby/object:Gem::Version
82
82
  version: '0'
83
83
  - !ruby/object:Gem::Dependency
84
- name: rubocop
84
+ name: rspec_junit_formatter
85
85
  requirement: !ruby/object:Gem::Requirement
86
86
  requirements:
87
87
  - - ">="
@@ -94,6 +94,20 @@ dependencies:
94
94
  - - ">="
95
95
  - !ruby/object:Gem::Version
96
96
  version: '0'
97
+ - !ruby/object:Gem::Dependency
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
97
111
  description:
98
112
  email:
99
113
  executables: []
@@ -103,6 +117,7 @@ files:
103
117
  - LICENSE
104
118
  - README.md
105
119
  - lib/fastlane/plugin/appcenter.rb
120
+ - lib/fastlane/plugin/appcenter/actions/appcenter_fetch_devices_action.rb
106
121
  - lib/fastlane/plugin/appcenter/actions/appcenter_upload_action.rb
107
122
  - lib/fastlane/plugin/appcenter/helper/appcenter_helper.rb
108
123
  - lib/fastlane/plugin/appcenter/version.rb