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.



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

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.


219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
# File 'lib/fastlane/plugin/firebase_app_distribution/client/firebase_app_distribution_api_client.rb', line 219

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

#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



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

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



93
94
95
96
97
98
99
100
101
102
103
104
# File 'lib/fastlane/plugin/firebase_app_distribution/client/firebase_app_distribution_api_client.rb', line 93

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



200
201
202
203
204
205
206
207
208
209
210
# File 'lib/fastlane/plugin/firebase_app_distribution/client/firebase_app_distribution_api_client.rb', line 200

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



186
187
188
189
190
191
192
# File 'lib/fastlane/plugin/firebase_app_distribution/client/firebase_app_distribution_api_client.rb', line 186

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.



266
267
268
269
270
271
272
273
274
275
276
277
# File 'lib/fastlane/plugin/firebase_app_distribution/client/firebase_app_distribution_api_client.rb', line 266

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

  return 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



246
247
248
249
250
251
252
253
254
255
256
# File 'lib/fastlane/plugin/firebase_app_distribution/client/firebase_app_distribution_api_client.rb', line 246

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

#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

Throws a user_error if the release_notes are invalid



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

def update_release_notes(release_name, release_notes)
  if release_notes.nil? || release_notes.empty?
    UI.success("✅ No release notes passed in. Skipping this step.")
    return
  end
  begin
    payload = {
      name: release_name,
      releaseNotes: {
        text: release_notes
      }
    }
    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
  rescue Faraday::ClientError => e
    error = ErrorResponse.new(e.response)
    UI.user_error!("#{ErrorMessage::INVALID_RELEASE_NOTES}: #{error.message}")
  end
  UI.success("✅ Posted release notes.")
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 the release_name of the uploaded release.

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



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
175
176
177
178
# File 'lib/fastlane/plugin/firebase_app_distribution/client/firebase_app_distribution_api_client.rb', line 143

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



115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
# File 'lib/fastlane/plugin/firebase_app_distribution/client/firebase_app_distribution_api_client.rb', line 115

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