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.
@@ -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
@@ -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 = 2
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, platform, debug = false)
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 group_ids are nil/empty).
28
+ # step if no testers are passed in (emails and group_aliases are nil/empty).
33
29
  #
34
30
  # args
35
- # app_id - Firebase App ID
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
- # group_ids - String array of Firebase tester group IDs
33
+ # group_aliases - String array of Firebase tester group aliases
39
34
  #
40
- # Throws a user_error if emails or group_ids are invalid
41
- def enable_access(app_id, release_id, emails, group_ids)
42
- if (emails.nil? || emails.empty?) && (group_ids.nil? || group_ids.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 = { emails: emails, groupIds: group_ids }
41
+ payload = { testerEmails: emails, groupAliases: group_aliases }
47
42
  begin
48
- connection.post(enable_access_url(app_id, release_id), payload.to_json) do |request|
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} \nGroups: #{group_ids}")
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
- # Posts notes for the specified app release. Skips this
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
- # app_id - Firebase App ID
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 post_notes(app_id, release_id, release_notes)
68
- payload = { releaseNotes: { releaseNotes: release_notes } }
69
- if release_notes.nil? || release_notes.empty?
70
- UI.success("✅ No release notes passed in. Skipping this step.")
71
- return
72
- end
73
- begin
74
- connection.post(release_notes_create_url(app_id, release_id), payload.to_json) do |request|
75
- request.headers[AUTHORIZATION] = "Bearer " + @auth_token
76
- request.headers[CONTENT_TYPE] = APPLICATION_JSON
77
- end
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
- # Returns the url encoded upload token used for get_upload_status calls:
86
- # projects/<project-number>/apps/<app-id>/releases/-/binaries/<binary-hash>
83
+ # Get AAB info (Android apps only)
87
84
  #
88
85
  # args
89
- # app_id - Firebase App ID
90
- # binary_path - Absolute path to your app's apk/ipa file
91
- #
92
- # Throws a user_error if an invalid app id is passed in, the binary file does
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(v1_apps_url(app_id)) do |request|
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}: #{app_id}")
96
+ UI.user_error!("#{ErrorMessage::INVALID_APP_ID}: #{app_name}")
106
97
  end
107
- contact_email = response.body[:contactEmail]
108
- if contact_email.nil? || contact_email.strip.empty?
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
- # app_id - Firebase App ID
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(app_id, binary_path, platform)
123
- connection.post(binary_upload_url(app_id), read_binary(binary_path)) do |request|
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["X-APP-DISTRO-API-CLIENT-ID"] = "fastlane"
127
- request.headers["X-APP-DISTRO-API-CLIENT-TYPE"] = platform
128
- request.headers["X-APP-DISTRO-API-CLIENT-VERSION"] = Fastlane::FirebaseAppDistribution::VERSION
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
- UI.user_error!("#{ErrorMessage.binary_not_found(@binary_type)}: #{binary_path}")
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
- # app_id - Firebase App ID
139
- # binary_path - Absolute path to your app's apk/ipa file
140
- #
141
- # Returns the release_id on a successful release, otherwise returns nil.
142
- #
143
- # Throws a UI error if the number of polling retries exceeds MAX_POLLING_RETRIES
144
- # Crashes if not able to upload the binary
145
- def upload(app_id, binary_path, platform)
146
- upload_token = get_upload_token(app_id, binary_path)
147
- upload_status_response = get_upload_status(app_id, upload_token)
148
- if upload_status_response.success? || upload_status_response.already_uploaded?
149
- UI.success(" This #{@binary_type} has been uploaded before. Skipping upload step.")
150
- else
151
- unless upload_status_response.in_progress?
152
- UI.message("⌛ Uploading the #{@binary_type}.")
153
- upload_binary(app_id, binary_path, platform)
154
- end
155
- MAX_POLLING_RETRIES.times do
156
- upload_status_response = get_upload_status(app_id, upload_token)
157
- if upload_status_response.success? || upload_status_response.already_uploaded?
158
- UI.success("✅ Uploaded the #{@binary_type}.")
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
- if !upload_status_response.message.nil?
164
- UI.user_error!("#{ErrorMessage.upload_binary_error(@binary_type)}: #{upload_status_response.message}")
165
- else
166
- UI.user_error!(ErrorMessage.upload_binary_error(@binary_type))
167
- end
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.release_id
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
- # app_id - Firebase App ID
182
- # upload_token - URL encoded upload token
179
+ # operation_name - Upload operation name (with binary hash)
183
180
  #
184
- # Returns the release ID on a successful release, otherwise returns nil.
185
- def get_upload_status(app_id, upload_token)
186
- response = connection.get(upload_status_url(app_id, upload_token)) do |request|
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
- return UploadStatusResponse.new(response.body)
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 v1_apps_url(app_id)
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 release_notes_create_url(app_id, release_id)
199
- "#{v1_apps_url(app_id)}/releases/#{release_id}/notes"
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 enable_access_url(app_id, release_id)
203
- "#{v1_apps_url(app_id)}/releases/#{release_id}/enable_access"
309
+ def list_releases_url(app_name)
310
+ "#{v1_apps_url(app_name)}/releases"
204
311
  end
205
312
 
206
- def binary_upload_url(app_id)
207
- "/app-binary-uploads?app_id=#{app_id}"
313
+ def get_udids_url(app_id)
314
+ "#{v1alpha_apps_url(app_id)}/testers:getTesterUdids"
208
315
  end
209
316
 
210
- def upload_status_url(app_id, app_token)
211
- "#{v1_apps_url(app_id)}/upload_status/#{app_token}"
317
+ def add_testers_url(project_number)
318
+ "/v1/projects/#{project_number}/testers:batchAdd"
212
319
  end
213
320
 
214
- def upload_token_format(app_id, project_number, binary_hash)
215
- CGI.escape("projects/#{project_number}/apps/#{app_id}/releases/-/binaries/#{binary_hash}")
321
+ def remove_testers_url(project_number)
322
+ "/v1/projects/#{project_number}/testers:batchRemove"
216
323
  end
217
324
 
218
325
  def connection