fastlane-plugin-firebase_app_distribution 0.2.5 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/fad-icon.png +0 -0
- data/lib/fastlane/plugin/firebase_app_distribution/actions/firebase_app_distribution_action.rb +143 -43
- data/lib/fastlane/plugin/firebase_app_distribution/actions/firebase_app_distribution_add_testers_action.rb +90 -0
- data/lib/fastlane/plugin/firebase_app_distribution/actions/firebase_app_distribution_get_latest_release.rb +114 -0
- data/lib/fastlane/plugin/firebase_app_distribution/actions/firebase_app_distribution_get_udids.rb +97 -0
- data/lib/fastlane/plugin/firebase_app_distribution/actions/firebase_app_distribution_remove_testers_action.rb +88 -0
- data/lib/fastlane/plugin/firebase_app_distribution/client/aab_info.rb +42 -0
- data/lib/fastlane/plugin/firebase_app_distribution/client/firebase_app_distribution_api_client.rb +223 -116
- data/lib/fastlane/plugin/firebase_app_distribution/helper/firebase_app_distribution_auth_client.rb +80 -34
- data/lib/fastlane/plugin/firebase_app_distribution/helper/firebase_app_distribution_error_message.rb +15 -5
- data/lib/fastlane/plugin/firebase_app_distribution/helper/firebase_app_distribution_helper.rb +26 -5
- data/lib/fastlane/plugin/firebase_app_distribution/helper/upload_status_response.rb +55 -15
- data/lib/fastlane/plugin/firebase_app_distribution/version.rb +1 -1
- metadata +9 -4
- data/lib/fastlane/plugin/firebase_app_distribution/actions/firebase_app_distribution_login.rb +0 -58
@@ -0,0 +1,88 @@
|
|
1
|
+
require 'fastlane/action'
|
2
|
+
require 'fastlane_core/ui/ui'
|
3
|
+
|
4
|
+
require_relative '../helper/firebase_app_distribution_helper'
|
5
|
+
require_relative '../helper/firebase_app_distribution_auth_client'
|
6
|
+
|
7
|
+
module Fastlane
|
8
|
+
module Actions
|
9
|
+
class FirebaseAppDistributionRemoveTestersAction < Action
|
10
|
+
extend Auth::FirebaseAppDistributionAuthClient
|
11
|
+
extend Helper::FirebaseAppDistributionHelper
|
12
|
+
|
13
|
+
def self.run(params)
|
14
|
+
auth_token = fetch_auth_token(params[:service_credentials_file], params[:firebase_cli_token])
|
15
|
+
fad_api_client = Client::FirebaseAppDistributionApiClient.new(auth_token, params[:debug])
|
16
|
+
|
17
|
+
if blank?(params[:emails]) && blank?(params[:file])
|
18
|
+
UI.user_error!("Must specify `emails` or `file`.")
|
19
|
+
end
|
20
|
+
|
21
|
+
emails = string_to_array(get_value_from_value_or_file(params[:emails], params[:file]))
|
22
|
+
|
23
|
+
UI.user_error!("Must pass at least one email") if blank?(emails)
|
24
|
+
|
25
|
+
if emails.count > 1000
|
26
|
+
UI.user_error!("A maximum of 1000 testers can be removed at a time.")
|
27
|
+
end
|
28
|
+
|
29
|
+
UI.message("⏳ Removing #{emails.count} testers from project #{params[:project_number]}...")
|
30
|
+
|
31
|
+
count = fad_api_client.remove_testers(params[:project_number], emails)
|
32
|
+
|
33
|
+
UI.success("✅ #{count} tester(s) removed successfully.")
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.description
|
37
|
+
"Delete testers in bulk from a comma-separated list or a file"
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.authors
|
41
|
+
["Tunde Agboola"]
|
42
|
+
end
|
43
|
+
|
44
|
+
# supports markdown.
|
45
|
+
def self.details
|
46
|
+
"Delete testers in bulk from a comma-separated list or a file"
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.available_options
|
50
|
+
[
|
51
|
+
FastlaneCore::ConfigItem.new(key: :project_number,
|
52
|
+
env_name: "FIREBASEAPPDISTRO_PROJECT_NUMBER",
|
53
|
+
description: "Your Firebase project number. You can find the project number in the Firebase console, on the General Settings page",
|
54
|
+
type: Integer,
|
55
|
+
optional: false),
|
56
|
+
FastlaneCore::ConfigItem.new(key: :emails,
|
57
|
+
env_name: "FIREBASEAPPDISTRO_REMOVE_TESTERS_EMAILS",
|
58
|
+
description: "Comma separated list of tester emails to be deleted. A maximum of 1000 testers can be deleted at a time",
|
59
|
+
optional: true,
|
60
|
+
type: String),
|
61
|
+
FastlaneCore::ConfigItem.new(key: :file,
|
62
|
+
env_name: "FIREBASEAPPDISTRO_REMOVE_TESTERS_FILE",
|
63
|
+
description: "Path to a file containing a comma separated list of tester emails to be deleted. A maximum of 1000 testers can be deleted at a time",
|
64
|
+
optional: true,
|
65
|
+
type: String),
|
66
|
+
FastlaneCore::ConfigItem.new(key: :service_credentials_file,
|
67
|
+
description: "Path to Google service credentials file",
|
68
|
+
optional: true,
|
69
|
+
type: String),
|
70
|
+
FastlaneCore::ConfigItem.new(key: :firebase_cli_token,
|
71
|
+
description: "Auth token generated using the Firebase CLI's login:ci command",
|
72
|
+
optional: true,
|
73
|
+
type: String),
|
74
|
+
FastlaneCore::ConfigItem.new(key: :debug,
|
75
|
+
description: "Print verbose debug output",
|
76
|
+
optional: true,
|
77
|
+
default_value: false,
|
78
|
+
is_string: false)
|
79
|
+
|
80
|
+
]
|
81
|
+
end
|
82
|
+
|
83
|
+
def self.is_supported?(platform)
|
84
|
+
true
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
class AabInfo
|
2
|
+
# AAB states
|
3
|
+
class AabState
|
4
|
+
UNSPECIFIED = 'AAB_STATE_UNSPECIFIED'
|
5
|
+
PLAY_ACCOUNT_NOT_LINKED = 'PLAY_ACCOUNT_NOT_LINKED'
|
6
|
+
NO_APP_WITH_GIVEN_BUNDLE_ID_IN_PLAY_ACCOUNT = 'NO_APP_WITH_GIVEN_BUNDLE_ID_IN_PLAY_ACCOUNT'
|
7
|
+
APP_NOT_PUBLISHED = 'APP_NOT_PUBLISHED'
|
8
|
+
PLAY_IAS_TERMS_NOT_ACCEPTED = 'PLAY_IAS_TERMS_NOT_ACCEPTED'
|
9
|
+
INTEGRATED = 'INTEGRATED'
|
10
|
+
UNAVAILABLE = 'AAB_STATE_UNAVAILABLE'
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialize(response)
|
14
|
+
@response = response || {}
|
15
|
+
end
|
16
|
+
|
17
|
+
def integration_state
|
18
|
+
@response[:integrationState]
|
19
|
+
end
|
20
|
+
|
21
|
+
def test_certificate
|
22
|
+
@response[:testCertificate] || {}
|
23
|
+
end
|
24
|
+
|
25
|
+
def md5_certificate_hash
|
26
|
+
test_certificate[:hashMd5]
|
27
|
+
end
|
28
|
+
|
29
|
+
def sha1_certificate_hash
|
30
|
+
test_certificate[:hashSha1]
|
31
|
+
end
|
32
|
+
|
33
|
+
def sha256_certificate_hash
|
34
|
+
test_certificate[:hashSha256]
|
35
|
+
end
|
36
|
+
|
37
|
+
def certs_provided?
|
38
|
+
(!md5_certificate_hash.nil? && !md5_certificate_hash.empty?) &&
|
39
|
+
(!sha1_certificate_hash.nil? && !sha1_certificate_hash.empty?) &&
|
40
|
+
(!sha256_certificate_hash.nil? && !sha256_certificate_hash.empty?)
|
41
|
+
end
|
42
|
+
end
|
data/lib/fastlane/plugin/firebase_app_distribution/client/firebase_app_distribution_api_client.rb
CHANGED
@@ -1,218 +1,325 @@
|
|
1
1
|
require 'fastlane_core/ui/ui'
|
2
|
-
require_relative '../actions/firebase_app_distribution_login'
|
3
2
|
require_relative '../client/error_response'
|
3
|
+
require_relative '../client/aab_info'
|
4
|
+
require_relative '../helper/firebase_app_distribution_helper'
|
4
5
|
|
5
6
|
module Fastlane
|
6
7
|
module Client
|
7
8
|
class FirebaseAppDistributionApiClient
|
9
|
+
include Helper::FirebaseAppDistributionHelper
|
10
|
+
|
8
11
|
BASE_URL = "https://firebaseappdistribution.googleapis.com"
|
9
12
|
TOKEN_CREDENTIAL_URI = "https://oauth2.googleapis.com/token"
|
10
13
|
MAX_POLLING_RETRIES = 60
|
11
|
-
POLLING_INTERVAL_SECONDS =
|
14
|
+
POLLING_INTERVAL_SECONDS = 5
|
12
15
|
|
13
16
|
AUTHORIZATION = "Authorization"
|
14
17
|
CONTENT_TYPE = "Content-Type"
|
15
18
|
APPLICATION_JSON = "application/json"
|
16
19
|
APPLICATION_OCTET_STREAM = "application/octet-stream"
|
20
|
+
CLIENT_VERSION = "X-Client-Version"
|
17
21
|
|
18
|
-
def initialize(auth_token,
|
22
|
+
def initialize(auth_token, debug = false)
|
19
23
|
@auth_token = auth_token
|
20
24
|
@debug = debug
|
21
|
-
|
22
|
-
if platform.nil?
|
23
|
-
@binary_type = "IPA/APK"
|
24
|
-
elsif platform == :ios
|
25
|
-
@binary_type = "IPA"
|
26
|
-
else
|
27
|
-
@binary_type = "APK"
|
28
|
-
end
|
29
25
|
end
|
30
26
|
|
31
27
|
# Enables tester access to the specified app release. Skips this
|
32
|
-
# step if no testers are passed in (emails and
|
28
|
+
# step if no testers are passed in (emails and group_aliases are nil/empty).
|
33
29
|
#
|
34
30
|
# args
|
35
|
-
#
|
36
|
-
# release_id - App release ID, returned by upload_status endpoint
|
31
|
+
# release_name - App release resource name, returned by upload_status endpoint
|
37
32
|
# emails - String array of app testers' email addresses
|
38
|
-
#
|
33
|
+
# group_aliases - String array of Firebase tester group aliases
|
39
34
|
#
|
40
|
-
# Throws a user_error if emails or
|
41
|
-
def
|
42
|
-
if (emails.nil? || emails.empty?) && (
|
35
|
+
# Throws a user_error if emails or group_aliases are invalid
|
36
|
+
def distribute(release_name, emails, group_aliases)
|
37
|
+
if (emails.nil? || emails.empty?) && (group_aliases.nil? || group_aliases.empty?)
|
43
38
|
UI.success("✅ No testers passed in. Skipping this step.")
|
44
39
|
return
|
45
40
|
end
|
46
|
-
payload = {
|
41
|
+
payload = { testerEmails: emails, groupAliases: group_aliases }
|
47
42
|
begin
|
48
|
-
connection.post(
|
43
|
+
connection.post(distribute_url(release_name), payload.to_json) do |request|
|
49
44
|
request.headers[AUTHORIZATION] = "Bearer " + @auth_token
|
50
45
|
request.headers[CONTENT_TYPE] = APPLICATION_JSON
|
46
|
+
request.headers[CLIENT_VERSION] = client_version_header_value
|
51
47
|
end
|
52
48
|
rescue Faraday::ClientError
|
53
|
-
UI.user_error!("#{ErrorMessage::INVALID_TESTERS} \nEmails: #{emails} \
|
49
|
+
UI.user_error!("#{ErrorMessage::INVALID_TESTERS} \nEmails: #{emails} \nGroup Aliases: #{group_aliases}")
|
54
50
|
end
|
55
51
|
UI.success("✅ Added testers/groups.")
|
56
52
|
end
|
57
53
|
|
58
|
-
#
|
54
|
+
# Update release notes for the specified app release. Skips this
|
59
55
|
# step if no notes are passed in (release_notes is nil/empty).
|
60
56
|
#
|
61
57
|
# args
|
62
|
-
#
|
63
|
-
# release_id - App release ID, returned by upload_status endpoint
|
58
|
+
# release_name - App release resource name, returned by upload_status endpoint
|
64
59
|
# release_notes - String of notes for this release
|
65
60
|
#
|
61
|
+
# Returns a hash of the release
|
62
|
+
#
|
66
63
|
# Throws a user_error if the release_notes are invalid
|
67
|
-
def
|
68
|
-
payload = {
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
rescue Faraday::ClientError => e
|
79
|
-
error = ErrorResponse.new(e.response)
|
80
|
-
UI.user_error!("#{ErrorMessage::INVALID_RELEASE_NOTES}: #{error.message}")
|
64
|
+
def update_release_notes(release_name, release_notes)
|
65
|
+
payload = {
|
66
|
+
name: release_name,
|
67
|
+
releaseNotes: {
|
68
|
+
text: release_notes
|
69
|
+
}
|
70
|
+
}
|
71
|
+
response = connection.patch(update_release_notes_url(release_name), payload.to_json) do |request|
|
72
|
+
request.headers[AUTHORIZATION] = "Bearer " + @auth_token
|
73
|
+
request.headers[CONTENT_TYPE] = APPLICATION_JSON
|
74
|
+
request.headers[CLIENT_VERSION] = client_version_header_value
|
81
75
|
end
|
82
76
|
UI.success("✅ Posted release notes.")
|
77
|
+
response.body
|
78
|
+
rescue Faraday::ClientError => e
|
79
|
+
error = ErrorResponse.new(e.response)
|
80
|
+
UI.user_error!("#{ErrorMessage::INVALID_RELEASE_NOTES}: #{error.message}")
|
83
81
|
end
|
84
82
|
|
85
|
-
#
|
86
|
-
# projects/<project-number>/apps/<app-id>/releases/-/binaries/<binary-hash>
|
83
|
+
# Get AAB info (Android apps only)
|
87
84
|
#
|
88
85
|
# args
|
89
|
-
#
|
90
|
-
#
|
91
|
-
#
|
92
|
-
|
93
|
-
# not exist, or invalid auth credentials are used (e.g. wrong project permissions)
|
94
|
-
def get_upload_token(app_id, binary_path)
|
95
|
-
if binary_path.nil? || !File.exist?(binary_path)
|
96
|
-
UI.crash!("#{ErrorMessage.binary_not_found(@binary_type)}: #{binary_path}")
|
97
|
-
end
|
98
|
-
binary_hash = Digest::SHA256.hexdigest(read_binary(binary_path))
|
99
|
-
|
86
|
+
# app_name - Firebase App resource name
|
87
|
+
#
|
88
|
+
# Throws a user_error if the app hasn't been onboarded to App Distribution
|
89
|
+
def get_aab_info(app_name)
|
100
90
|
begin
|
101
|
-
response = connection.get(
|
91
|
+
response = connection.get(aab_info_url(app_name)) do |request|
|
102
92
|
request.headers[AUTHORIZATION] = "Bearer " + @auth_token
|
93
|
+
request.headers[CLIENT_VERSION] = client_version_header_value
|
103
94
|
end
|
104
95
|
rescue Faraday::ResourceNotFound
|
105
|
-
UI.user_error!("#{ErrorMessage::INVALID_APP_ID}: #{
|
96
|
+
UI.user_error!("#{ErrorMessage::INVALID_APP_ID}: #{app_name}")
|
106
97
|
end
|
107
|
-
|
108
|
-
|
109
|
-
UI.user_error!(ErrorMessage::GET_APP_NO_CONTACT_EMAIL_ERROR)
|
110
|
-
end
|
111
|
-
return upload_token_format(response.body[:appId], response.body[:projectNumber], binary_hash)
|
98
|
+
|
99
|
+
AabInfo.new(response.body)
|
112
100
|
end
|
113
101
|
|
114
102
|
# Uploads the app binary to the Firebase API
|
115
103
|
#
|
116
104
|
# args
|
117
|
-
#
|
118
|
-
# binary_path - Absolute path to your app's apk/ipa file
|
105
|
+
# app_name - Firebase App resource name
|
106
|
+
# binary_path - Absolute path to your app's aab/apk/ipa file
|
119
107
|
# platform - 'android' or 'ios'
|
108
|
+
# timeout - The amount of seconds before the upload will timeout, if not completed
|
120
109
|
#
|
121
110
|
# Throws a user_error if the binary file does not exist
|
122
|
-
def upload_binary(
|
123
|
-
connection.post(binary_upload_url(
|
111
|
+
def upload_binary(app_name, binary_path, platform, timeout)
|
112
|
+
response = connection.post(binary_upload_url(app_name), read_binary(binary_path)) do |request|
|
113
|
+
request.options.timeout = timeout # seconds
|
124
114
|
request.headers[AUTHORIZATION] = "Bearer " + @auth_token
|
125
115
|
request.headers[CONTENT_TYPE] = APPLICATION_OCTET_STREAM
|
126
|
-
request.headers[
|
127
|
-
request.headers["X-
|
128
|
-
request.headers["X-
|
116
|
+
request.headers[CLIENT_VERSION] = client_version_header_value
|
117
|
+
request.headers["X-Goog-Upload-File-Name"] = File.basename(binary_path)
|
118
|
+
request.headers["X-Goog-Upload-Protocol"] = "raw"
|
129
119
|
end
|
120
|
+
|
121
|
+
response.body[:name] || ''
|
130
122
|
rescue Errno::ENOENT # Raised when binary_path file does not exist
|
131
|
-
|
123
|
+
binary_type = binary_type_from_path(binary_path)
|
124
|
+
UI.user_error!("#{ErrorMessage.binary_not_found(binary_type)}: #{binary_path}")
|
132
125
|
end
|
133
126
|
|
134
127
|
# Uploads the binary file if it has not already been uploaded
|
135
128
|
# Takes at least POLLING_INTERVAL_SECONDS between polling get_upload_status
|
136
129
|
#
|
137
130
|
# args
|
138
|
-
#
|
139
|
-
# binary_path - Absolute path to your app's apk/ipa file
|
140
|
-
#
|
141
|
-
#
|
142
|
-
#
|
143
|
-
#
|
144
|
-
# Crashes if
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
131
|
+
# app_name - Firebase App resource name
|
132
|
+
# binary_path - Absolute path to your app's aab/apk/ipa file
|
133
|
+
# timeout - The amount of seconds before the upload will timeout, if not completed
|
134
|
+
#
|
135
|
+
# Returns a `UploadStatusResponse` with the upload is complete.
|
136
|
+
#
|
137
|
+
# Crashes if the number of polling retries exceeds MAX_POLLING_RETRIES or if the binary cannot
|
138
|
+
# be uploaded.
|
139
|
+
def upload(app_name, binary_path, platform, timeout)
|
140
|
+
binary_type = binary_type_from_path(binary_path)
|
141
|
+
|
142
|
+
UI.message("⌛ Uploading the #{binary_type}.")
|
143
|
+
operation_name = upload_binary(app_name, binary_path, platform, timeout)
|
144
|
+
|
145
|
+
upload_status_response = get_upload_status(operation_name)
|
146
|
+
MAX_POLLING_RETRIES.times do
|
147
|
+
if upload_status_response.success?
|
148
|
+
if upload_status_response.release_updated?
|
149
|
+
UI.success("✅ Uploaded #{binary_type} successfully; updated provisioning profile of existing release #{upload_status_response.release_version}.")
|
150
|
+
break
|
151
|
+
elsif upload_status_response.release_unmodified?
|
152
|
+
UI.success("✅ The same #{binary_type} was found in release #{upload_status_response.release_version} with no changes, skipping.")
|
159
153
|
break
|
160
|
-
elsif upload_status_response.in_progress?
|
161
|
-
sleep(POLLING_INTERVAL_SECONDS)
|
162
154
|
else
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
155
|
+
UI.success("✅ Uploaded #{binary_type} successfully and created release #{upload_status_response.release_version}.")
|
156
|
+
end
|
157
|
+
break
|
158
|
+
elsif upload_status_response.in_progress?
|
159
|
+
sleep(POLLING_INTERVAL_SECONDS)
|
160
|
+
upload_status_response = get_upload_status(operation_name)
|
161
|
+
else
|
162
|
+
if !upload_status_response.error_message.nil?
|
163
|
+
UI.user_error!("#{ErrorMessage.upload_binary_error(binary_type)}: #{upload_status_response.error_message}")
|
164
|
+
else
|
165
|
+
UI.user_error!(ErrorMessage.upload_binary_error(binary_type))
|
168
166
|
end
|
169
|
-
end
|
170
|
-
unless upload_status_response.success?
|
171
|
-
UI.error("It took longer than expected to process your #{@binary_type}, please try again.")
|
172
|
-
return nil
|
173
167
|
end
|
174
168
|
end
|
175
|
-
upload_status_response.
|
169
|
+
unless upload_status_response.success?
|
170
|
+
UI.crash!("It took longer than expected to process your #{binary_type}, please try again.")
|
171
|
+
end
|
172
|
+
|
173
|
+
upload_status_response
|
176
174
|
end
|
177
175
|
|
178
176
|
# Fetches the status of an uploaded binary
|
179
177
|
#
|
180
178
|
# args
|
181
|
-
#
|
182
|
-
# upload_token - URL encoded upload token
|
179
|
+
# operation_name - Upload operation name (with binary hash)
|
183
180
|
#
|
184
|
-
# Returns the
|
185
|
-
def get_upload_status(
|
186
|
-
response = connection.get(upload_status_url(
|
181
|
+
# Returns the `done` status, as well as a release, error, or nil
|
182
|
+
def get_upload_status(operation_name)
|
183
|
+
response = connection.get(upload_status_url(operation_name)) do |request|
|
187
184
|
request.headers[AUTHORIZATION] = "Bearer " + @auth_token
|
185
|
+
request.headers[CLIENT_VERSION] = client_version_header_value
|
188
186
|
end
|
189
|
-
|
187
|
+
UploadStatusResponse.new(response.body)
|
188
|
+
end
|
189
|
+
|
190
|
+
# Get tester UDIDs
|
191
|
+
#
|
192
|
+
# args
|
193
|
+
# app_name - Firebase App resource name
|
194
|
+
#
|
195
|
+
# Returns a list of hashes containing tester device info
|
196
|
+
def get_udids(app_id)
|
197
|
+
begin
|
198
|
+
response = connection.get(get_udids_url(app_id)) do |request|
|
199
|
+
request.headers[AUTHORIZATION] = "Bearer " + @auth_token
|
200
|
+
request.headers[CLIENT_VERSION] = client_version_header_value
|
201
|
+
end
|
202
|
+
rescue Faraday::ResourceNotFound
|
203
|
+
UI.user_error!("#{ErrorMessage::INVALID_APP_ID}: #{app_id}")
|
204
|
+
end
|
205
|
+
response.body[:testerUdids] || []
|
206
|
+
end
|
207
|
+
|
208
|
+
# Create testers
|
209
|
+
#
|
210
|
+
# args
|
211
|
+
# project_number - Firebase project number
|
212
|
+
# emails - An array of emails to be created as testers. A maximum of
|
213
|
+
# 1000 testers can be created at a time.
|
214
|
+
#
|
215
|
+
def add_testers(project_number, emails)
|
216
|
+
payload = { emails: emails }
|
217
|
+
connection.post(add_testers_url(project_number), payload.to_json) do |request|
|
218
|
+
request.headers[AUTHORIZATION] = "Bearer " + @auth_token
|
219
|
+
request.headers[CONTENT_TYPE] = APPLICATION_JSON
|
220
|
+
request.headers[CLIENT_VERSION] = client_version_header_value
|
221
|
+
end
|
222
|
+
rescue Faraday::BadRequestError
|
223
|
+
UI.user_error!(ErrorMessage::INVALID_EMAIL_ADDRESS)
|
224
|
+
rescue Faraday::ResourceNotFound
|
225
|
+
UI.user_error!(ErrorMessage::INVALID_PROJECT)
|
226
|
+
rescue Faraday::ClientError => e
|
227
|
+
if e.response[:status] == 429
|
228
|
+
UI.user_error!(ErrorMessage::TESTER_LIMIT_VIOLATION)
|
229
|
+
else
|
230
|
+
raise e
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
234
|
+
# Delete testers
|
235
|
+
#
|
236
|
+
# args
|
237
|
+
# project_number - Firebase project number
|
238
|
+
# emails - An array of emails to be deleted as testers. A maximum of
|
239
|
+
# 1000 testers can be deleted at a time.
|
240
|
+
#
|
241
|
+
# Returns the number of testers that were deleted
|
242
|
+
def remove_testers(project_number, emails)
|
243
|
+
payload = { emails: emails }
|
244
|
+
response = connection.post(remove_testers_url(project_number), payload.to_json) do |request|
|
245
|
+
request.headers[AUTHORIZATION] = "Bearer " + @auth_token
|
246
|
+
request.headers[CONTENT_TYPE] = APPLICATION_JSON
|
247
|
+
request.headers[CLIENT_VERSION] = client_version_header_value
|
248
|
+
end
|
249
|
+
response.body[:emails] ? response.body[:emails].count : 0
|
250
|
+
rescue Faraday::ResourceNotFound
|
251
|
+
UI.user_error!(ErrorMessage::INVALID_PROJECT)
|
252
|
+
end
|
253
|
+
|
254
|
+
# List releases
|
255
|
+
#
|
256
|
+
# args
|
257
|
+
# app_name - Firebase App resource name
|
258
|
+
# page_size - The number of releases to return in the page
|
259
|
+
# page_token - A page token, received from a previous call
|
260
|
+
#
|
261
|
+
# Returns the response body. Throws a user_error if the app hasn't been onboarded to App Distribution.
|
262
|
+
def list_releases(app_name, page_size = 100, page_token = nil)
|
263
|
+
begin
|
264
|
+
response = connection.get(list_releases_url(app_name), { pageSize: page_size.to_s, pageToken: page_token }) do |request|
|
265
|
+
request.headers[AUTHORIZATION] = "Bearer " + @auth_token
|
266
|
+
request.headers[CLIENT_VERSION] = client_version_header_value
|
267
|
+
end
|
268
|
+
rescue Faraday::ResourceNotFound
|
269
|
+
UI.user_error!("#{ErrorMessage::INVALID_APP_ID}: #{app_name}")
|
270
|
+
end
|
271
|
+
|
272
|
+
return response.body
|
190
273
|
end
|
191
274
|
|
192
275
|
private
|
193
276
|
|
194
|
-
def
|
277
|
+
def client_version_header_value
|
278
|
+
"fastlane/#{Fastlane::FirebaseAppDistribution::VERSION}"
|
279
|
+
end
|
280
|
+
|
281
|
+
def v1alpha_apps_url(app_id)
|
195
282
|
"/v1alpha/apps/#{app_id}"
|
196
283
|
end
|
197
284
|
|
198
|
-
def
|
199
|
-
"
|
285
|
+
def v1_apps_url(app_name)
|
286
|
+
"/v1/#{app_name}"
|
287
|
+
end
|
288
|
+
|
289
|
+
def aab_info_url(app_name)
|
290
|
+
"#{v1_apps_url(app_name)}/aabInfo"
|
291
|
+
end
|
292
|
+
|
293
|
+
def update_release_notes_url(release_name)
|
294
|
+
"/v1/#{release_name}?updateMask=release_notes.text"
|
295
|
+
end
|
296
|
+
|
297
|
+
def distribute_url(release_name)
|
298
|
+
"/v1/#{release_name}:distribute"
|
299
|
+
end
|
300
|
+
|
301
|
+
def binary_upload_url(app_name)
|
302
|
+
"/upload#{v1_apps_url(app_name)}/releases:upload"
|
303
|
+
end
|
304
|
+
|
305
|
+
def upload_status_url(operation_name)
|
306
|
+
"/v1/#{operation_name}"
|
200
307
|
end
|
201
308
|
|
202
|
-
def
|
203
|
-
"#{v1_apps_url(
|
309
|
+
def list_releases_url(app_name)
|
310
|
+
"#{v1_apps_url(app_name)}/releases"
|
204
311
|
end
|
205
312
|
|
206
|
-
def
|
207
|
-
"
|
313
|
+
def get_udids_url(app_id)
|
314
|
+
"#{v1alpha_apps_url(app_id)}/testers:getTesterUdids"
|
208
315
|
end
|
209
316
|
|
210
|
-
def
|
211
|
-
"
|
317
|
+
def add_testers_url(project_number)
|
318
|
+
"/v1/projects/#{project_number}/testers:batchAdd"
|
212
319
|
end
|
213
320
|
|
214
|
-
def
|
215
|
-
|
321
|
+
def remove_testers_url(project_number)
|
322
|
+
"/v1/projects/#{project_number}/testers:batchRemove"
|
216
323
|
end
|
217
324
|
|
218
325
|
def connection
|