Class: Fastlane::Client::FirebaseAppDistributionApiClient

Inherits:
Object
  • Object
show all
Includes:
Helper::FirebaseAppDistributionHelper
Defined in:
lib/fastlane/plugin/firebase_app_distribution/client/firebase_app_distribution_api_client.rb

Constant Summary collapse

BASE_URL =
"https://firebaseappdistribution.googleapis.com"
TOKEN_CREDENTIAL_URI =
"https://oauth2.googleapis.com/token"
MAX_POLLING_RETRIES =
60
POLLING_INTERVAL_SECONDS =
5
AUTHORIZATION =
"Authorization"
CONTENT_TYPE =
"Content-Type"
APPLICATION_JSON =
"application/json"
APPLICATION_OCTET_STREAM =
"application/octet-stream"
CLIENT_VERSION =
"X-Client-Version"

Instance Method Summary collapse

Methods included from Helper::FirebaseAppDistributionHelper

#app_name_from_app_id, #binary_type_from_path, #blank?, #get_ios_app_id_from_archive_plist, #get_value_from_value_or_file, #parse_plist, #present?, #string_to_array

Constructor Details

#initialize(auth_token, debug = false) ⇒ FirebaseAppDistributionApiClient

Returns a new instance of FirebaseAppDistributionApiClient.



22
23
24
25
# File 'lib/fastlane/plugin/firebase_app_distribution/client/firebase_app_distribution_api_client.rb', line 22

def initialize(auth_token, debug = false)
  @auth_token = auth_token
  @debug = debug
end

Instance Method Details

#add_testers(project_number, emails) ⇒ Object

Create testers

args

project_number - Firebase project number
emails - An array of emails to be created as testers. A maximum of
         1000 testers can be created at a time.


215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
# File 'lib/fastlane/plugin/firebase_app_distribution/client/firebase_app_distribution_api_client.rb', line 215

def add_testers(project_number, emails)
  payload = { emails: emails }
  connection.post(add_testers_url(project_number), payload.to_json) do |request|
    request.headers[AUTHORIZATION] = "Bearer " + @auth_token
    request.headers[CONTENT_TYPE] = APPLICATION_JSON
    request.headers[CLIENT_VERSION] = client_version_header_value
  end
rescue Faraday::BadRequestError
  UI.user_error!(ErrorMessage::INVALID_EMAIL_ADDRESS)
rescue Faraday::ResourceNotFound
  UI.user_error!(ErrorMessage::INVALID_PROJECT)
rescue Faraday::ClientError => e
  if e.response[:status] == 429
    UI.user_error!(ErrorMessage::TESTER_LIMIT_VIOLATION)
  else
    raise e
  end
end

#add_testers_to_group(project_number, group_alias, emails, create_missing_testers = false) ⇒ Object

Add testers to group

args

project_number - Firebase project number
group_alias - Alias of the tester group
emails - An array of emails to be added to the group.
         A maximum of 1000 testers can be added at a time, if creating missing testers is enabled.
create_missing_testers - If true, missing testers will be created and added to the group.


293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
# File 'lib/fastlane/plugin/firebase_app_distribution/client/firebase_app_distribution_api_client.rb', line 293

def add_testers_to_group(project_number, group_alias, emails, create_missing_testers = false)
  payload = { emails: emails,
              createMissingTesters: create_missing_testers }
  response = connection.post(add_testers_to_group_url(project_number, group_alias), payload.to_json) do |request|
    request.headers[AUTHORIZATION] = "Bearer " + @auth_token
    request.headers[CONTENT_TYPE] = APPLICATION_JSON
    request.headers[CLIENT_VERSION] = client_version_header_value
  end
  response.body
rescue Faraday::BadRequestError
  UI.user_error!(ErrorMessage::INVALID_EMAIL_ADDRESS)
rescue Faraday::ResourceNotFound
  UI.user_error!(ErrorMessage::INVALID_TESTER_GROUP)
rescue Faraday::ClientError => e
  raise e
end

#create_group(project_number, group_alias, display_name) ⇒ Object

Create tester group

args

project_number - Firebase project number
group_alias - Alias of the tester group
display_name - Display name of the tester group


261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
# File 'lib/fastlane/plugin/firebase_app_distribution/client/firebase_app_distribution_api_client.rb', line 261

def create_group(project_number, group_alias, display_name)
  payload = { name: "projects/#{project_number}/groups/#{group_alias}",
              displayName: display_name }
  response = connection.post(add_tester_group_url(project_number), payload.to_json) do |request|
    request.params["groupId"] = group_alias
    request.headers[AUTHORIZATION] = "Bearer " + @auth_token
    request.headers[CONTENT_TYPE] = APPLICATION_JSON
    request.headers[CLIENT_VERSION] = client_version_header_value
  end
  response.body
rescue Faraday::BadRequestError
  UI.user_error!(ErrorMessage::INVALID_TESTER_GROUP_NAME)
rescue Faraday::ResourceNotFound
  UI.user_error!(ErrorMessage::INVALID_PROJECT)
rescue Faraday::ConflictError
  UI.important("Tester group #{group_alias} already exists.")
  return {
    name: "projects/#{project_number}/groups/#{group_alias}"
  }
rescue Faraday::ClientError => e
  raise e
end

#delete_group(project_number, group_alias) ⇒ Object

Delete tester group

args

project_number - Firebase project number
group_alias - Alias of the tester group


339
340
341
342
343
344
345
346
347
348
# File 'lib/fastlane/plugin/firebase_app_distribution/client/firebase_app_distribution_api_client.rb', line 339

def delete_group(project_number, group_alias)
  response = connection.delete(delete_tester_group_url(project_number, group_alias)) do |request|
    request.headers[AUTHORIZATION] = "Bearer " + @auth_token
    request.headers[CONTENT_TYPE] = APPLICATION_JSON
    request.headers[CLIENT_VERSION] = client_version_header_value
  end
  response.body
rescue Faraday::ResourceNotFound
  UI.user_error!(ErrorMessage::INVALID_TESTER_GROUP)
end

#distribute(release_name, emails, group_aliases) ⇒ Object

Enables tester access to the specified app release. Skips this step if no testers are passed in (emails and group_aliases are nil/empty).

args

release_name - App release resource name, returned by upload_status endpoint
emails - String array of app testers' email addresses
group_aliases - String array of Firebase tester group aliases

Throws a user_error if emails or group_aliases are invalid



36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# File 'lib/fastlane/plugin/firebase_app_distribution/client/firebase_app_distribution_api_client.rb', line 36

def distribute(release_name, emails, group_aliases)
  if (emails.nil? || emails.empty?) && (group_aliases.nil? || group_aliases.empty?)
    UI.success("✅ No testers passed in. Skipping this step.")
    return
  end
  payload = { testerEmails: emails, groupAliases: group_aliases }
  begin
    connection.post(distribute_url(release_name), payload.to_json) do |request|
      request.headers[AUTHORIZATION] = "Bearer " + @auth_token
      request.headers[CONTENT_TYPE] = APPLICATION_JSON
      request.headers[CLIENT_VERSION] = client_version_header_value
    end
  rescue Faraday::ClientError
    UI.user_error!("#{ErrorMessage::INVALID_TESTERS} \nEmails: #{emails} \nGroup Aliases: #{group_aliases}")
  end
  UI.success("✅ Added testers/groups.")
end

#get_aab_info(app_name) ⇒ Object

Get AAB info (Android apps only)

args

app_name - Firebase App resource name

Throws a user_error if the app hasn’t been onboarded to App Distribution



89
90
91
92
93
94
95
96
97
98
99
100
# File 'lib/fastlane/plugin/firebase_app_distribution/client/firebase_app_distribution_api_client.rb', line 89

def get_aab_info(app_name)
  begin
    response = connection.get(aab_info_url(app_name)) do |request|
      request.headers[AUTHORIZATION] = "Bearer " + @auth_token
      request.headers[CLIENT_VERSION] = client_version_header_value
    end
  rescue Faraday::ResourceNotFound
    UI.user_error!("#{ErrorMessage::INVALID_APP_ID}: #{app_name}")
  end

  AabInfo.new(response.body)
end

#get_udids(app_id) ⇒ Object

Get tester UDIDs

args

app_name - Firebase App resource name

Returns a list of hashes containing tester device info



196
197
198
199
200
201
202
203
204
205
206
# File 'lib/fastlane/plugin/firebase_app_distribution/client/firebase_app_distribution_api_client.rb', line 196

def get_udids(app_id)
  begin
    response = connection.get(get_udids_url(app_id)) do |request|
      request.headers[AUTHORIZATION] = "Bearer " + @auth_token
      request.headers[CLIENT_VERSION] = client_version_header_value
    end
  rescue Faraday::ResourceNotFound
    UI.user_error!("#{ErrorMessage::INVALID_APP_ID}: #{app_id}")
  end
  response.body[:testerUdids] || []
end

#get_upload_status(operation_name) ⇒ Object

Fetches the status of an uploaded binary

args

operation_name - Upload operation name (with binary hash)

Returns the ‘done` status, as well as a release, error, or nil



182
183
184
185
186
187
188
# File 'lib/fastlane/plugin/firebase_app_distribution/client/firebase_app_distribution_api_client.rb', line 182

def get_upload_status(operation_name)
  response = connection.get(upload_status_url(operation_name)) do |request|
    request.headers[AUTHORIZATION] = "Bearer " + @auth_token
    request.headers[CLIENT_VERSION] = client_version_header_value
  end
  UploadStatusResponse.new(response.body)
end

#list_releases(app_name, page_size = 100, page_token = nil) ⇒ Object

List releases

args

app_name - Firebase App resource name
page_size - The number of releases to return in the page
page_token - A page token, received from a previous call

Returns the response body. Throws a user_error if the app hasn’t been onboarded to App Distribution.



358
359
360
361
362
363
364
365
366
367
368
369
# File 'lib/fastlane/plugin/firebase_app_distribution/client/firebase_app_distribution_api_client.rb', line 358

def list_releases(app_name, page_size = 100, page_token = nil)
  begin
    response = connection.get(list_releases_url(app_name), { pageSize: page_size.to_s, pageToken: page_token }) do |request|
      request.headers[AUTHORIZATION] = "Bearer " + @auth_token
      request.headers[CLIENT_VERSION] = client_version_header_value
    end
  rescue Faraday::ResourceNotFound
    UI.user_error!("#{ErrorMessage::INVALID_APP_ID}: #{app_name}")
  end

  response.body
end

#remove_testers(project_number, emails) ⇒ Object

Delete testers

args

project_number - Firebase project number
emails - An array of emails to be deleted as testers. A maximum of
         1000 testers can be deleted at a time.

Returns the number of testers that were deleted



242
243
244
245
246
247
248
249
250
251
252
# File 'lib/fastlane/plugin/firebase_app_distribution/client/firebase_app_distribution_api_client.rb', line 242

def remove_testers(project_number, emails)
  payload = { emails: emails }
  response = connection.post(remove_testers_url(project_number), payload.to_json) do |request|
    request.headers[AUTHORIZATION] = "Bearer " + @auth_token
    request.headers[CONTENT_TYPE] = APPLICATION_JSON
    request.headers[CLIENT_VERSION] = client_version_header_value
  end
  response.body[:emails] ? response.body[:emails].count : 0
rescue Faraday::ResourceNotFound
  UI.user_error!(ErrorMessage::INVALID_PROJECT)
end

#remove_testers_from_group(project_number, group_alias, emails) ⇒ Object

Remove testers from group

args

project_number - Firebase project number
group_alias - Alias of the tester group
emails - An array of emails to be removed from the group.


317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
# File 'lib/fastlane/plugin/firebase_app_distribution/client/firebase_app_distribution_api_client.rb', line 317

def remove_testers_from_group(project_number, group_alias, emails)
  payload = { emails: emails }
  response = connection.post(remove_testers_from_group_url(project_number, group_alias), payload.to_json) do |request|
    request.headers[AUTHORIZATION] = "Bearer " + @auth_token
    request.headers[CONTENT_TYPE] = APPLICATION_JSON
    request.headers[CLIENT_VERSION] = client_version_header_value
  end
  response.body
rescue Faraday::BadRequestError
  UI.user_error!(ErrorMessage::INVALID_EMAIL_ADDRESS)
rescue Faraday::ResourceNotFound
  UI.user_error!(ErrorMessage::INVALID_TESTER_GROUP)
rescue Faraday::ClientError => e
  raise e
end

#update_release_notes(release_name, release_notes) ⇒ Object

Update release notes for the specified app release. Skips this step if no notes are passed in (release_notes is nil/empty).

args

release_name - App release resource name, returned by upload_status endpoint
release_notes - String of notes for this release

Returns a hash of the release

Throws a user_error if the release_notes are invalid



64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
# File 'lib/fastlane/plugin/firebase_app_distribution/client/firebase_app_distribution_api_client.rb', line 64

def update_release_notes(release_name, release_notes)
  payload = {
    name: release_name,
    releaseNotes: {
      text: release_notes
    }
  }
  response = connection.patch(update_release_notes_url(release_name), payload.to_json) do |request|
    request.headers[AUTHORIZATION] = "Bearer " + @auth_token
    request.headers[CONTENT_TYPE] = APPLICATION_JSON
    request.headers[CLIENT_VERSION] = client_version_header_value
  end
  UI.success("✅ Posted release notes.")
  response.body
rescue Faraday::ClientError => e
  error = ErrorResponse.new(e.response)
  UI.user_error!("#{ErrorMessage::INVALID_RELEASE_NOTES}: #{error.message}")
end

#upload(app_name, binary_path, platform, timeout) ⇒ Object

Uploads the binary file if it has not already been uploaded Takes at least POLLING_INTERVAL_SECONDS between polling get_upload_status

args

app_name - Firebase App resource name
binary_path - Absolute path to your app's aab/apk/ipa file
timeout - The amount of seconds before the upload will timeout, if not completed

Returns a ‘UploadStatusResponse` with the upload is complete.

Crashes if the number of polling retries exceeds MAX_POLLING_RETRIES or if the binary cannot be uploaded.



139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
# File 'lib/fastlane/plugin/firebase_app_distribution/client/firebase_app_distribution_api_client.rb', line 139

def upload(app_name, binary_path, platform, timeout)
  binary_type = binary_type_from_path(binary_path)

  UI.message("⌛ Uploading the #{binary_type}.")
  operation_name = upload_binary(app_name, binary_path, platform, timeout)

  upload_status_response = get_upload_status(operation_name)
  MAX_POLLING_RETRIES.times do
    if upload_status_response.success?
      if upload_status_response.release_updated?
        UI.success("✅ Uploaded #{binary_type} successfully; updated provisioning profile of existing release #{upload_status_response.release_version}.")
        break
      elsif upload_status_response.release_unmodified?
        UI.success("✅ The same #{binary_type} was found in release #{upload_status_response.release_version} with no changes, skipping.")
        break
      else
        UI.success("✅ Uploaded #{binary_type} successfully and created release #{upload_status_response.release_version}.")
      end
      break
    elsif upload_status_response.in_progress?
      sleep(POLLING_INTERVAL_SECONDS)
      upload_status_response = get_upload_status(operation_name)
    else
      if !upload_status_response.error_message.nil?
        UI.user_error!("#{ErrorMessage.upload_binary_error(binary_type)}: #{upload_status_response.error_message}")
      else
        UI.user_error!(ErrorMessage.upload_binary_error(binary_type))
      end
    end
  end
  unless upload_status_response.success?
    UI.crash!("It took longer than expected to process your #{binary_type}, please try again.")
  end

  upload_status_response
end

#upload_binary(app_name, binary_path, platform, timeout) ⇒ Object

Uploads the app binary to the Firebase API

args

app_name - Firebase App resource name
binary_path - Absolute path to your app's aab/apk/ipa file
platform - 'android' or 'ios'
timeout - The amount of seconds before the upload will timeout, if not completed

Throws a user_error if the binary file does not exist



111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
# File 'lib/fastlane/plugin/firebase_app_distribution/client/firebase_app_distribution_api_client.rb', line 111

def upload_binary(app_name, binary_path, platform, timeout)
  response = connection.post(binary_upload_url(app_name), read_binary(binary_path)) do |request|
    request.options.timeout = timeout # seconds
    request.headers[AUTHORIZATION] = "Bearer " + @auth_token
    request.headers[CONTENT_TYPE] = APPLICATION_OCTET_STREAM
    request.headers[CLIENT_VERSION] = client_version_header_value
    request.headers["X-Goog-Upload-File-Name"] = File.basename(binary_path)
    request.headers["X-Goog-Upload-Protocol"] = "raw"
  end

  response.body[:name] || ''
rescue Errno::ENOENT # Raised when binary_path file does not exist
  binary_type = binary_type_from_path(binary_path)
  UI.user_error!("#{ErrorMessage.binary_not_found(binary_type)}: #{binary_path}")
end