Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Prefer Google auth library and generated API client for list releases action #307

Merged
merged 5 commits into from Jul 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 0 additions & 1 deletion Gemfile
@@ -1,7 +1,6 @@
source('https://rubygems.org')

gemspec
gem 'google-api-client', '~> 0.38'

plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile')
eval_gemfile(plugins_path) if File.exist?(plugins_path)
3 changes: 3 additions & 0 deletions fastlane-plugin-firebase_app_distribution.gemspec
Expand Up @@ -18,6 +18,9 @@ Gem::Specification.new do |spec|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
spec.require_paths = ['lib']

spec.add_dependency('google-api-client', '~> 0.38')
spec.add_dependency('google-apis-firebaseappdistribution_v1', '~> 0.3.0')

spec.add_development_dependency('pry')
spec.add_development_dependency('bundler')
spec.add_development_dependency('rspec')
Expand Down
@@ -1,5 +1,5 @@
require 'fastlane/action'
require_relative '../client/firebase_app_distribution_api_client'
require 'google/apis/firebaseappdistribution_v1'
require_relative '../helper/firebase_app_distribution_auth_client'
require_relative '../helper/firebase_app_distribution_helper'

Expand All @@ -12,24 +12,58 @@ class FirebaseAppDistributionGetLatestReleaseAction < Action
extend Auth::FirebaseAppDistributionAuthClient
extend Helper::FirebaseAppDistributionHelper

FirebaseAppDistribution = Google::Apis::FirebaseappdistributionV1

def self.run(params)
auth_token = fetch_auth_token(params[:service_credentials_file], params[:firebase_cli_token])
fad_api_client = Client::FirebaseAppDistributionApiClient.new(auth_token, params[:debug])
client = FirebaseAppDistribution::FirebaseAppDistributionService.new
client.authorization =
get_authorization(params[:service_credentials_file], params[:firebase_cli_token])

UI.message("⏳ Fetching latest release for app #{params[:app]}...")

releases = fad_api_client.list_releases(app_name_from_app_id(params[:app]), 1)[:releases] || []
if releases.empty?
parent = app_name_from_app_id(params[:app])

begin
releases = client.list_project_app_releases(parent, page_size: 1).releases
rescue Google::Apis::Error => err
if err.status_code.to_i == 404
UI.user_error!("#{ErrorMessage::INVALID_APP_ID}: #{params[:app]}")
else
UI.crash!(err)
end
end

if releases.nil? || releases.empty?
latest_release = nil
UI.important("No releases for app #{params[:app]} found in App Distribution. Returning nil and setting Actions.lane_context[SharedValues::FIREBASE_APP_DISTRO_LATEST_RELEASE].")
else
latest_release = releases[0]
# latest_release = append_json_style_fields(response.releases[0].to_h)
latest_release = map_release_hash(releases[0])
UI.success("✅ Latest release fetched successfully. Returning release and setting Actions.lane_context[SharedValues::FIREBASE_APP_DISTRO_LATEST_RELEASE].")
end
Actions.lane_context[SharedValues::FIREBASE_APP_DISTRO_LATEST_RELEASE] = latest_release
return latest_release
end

def self.map_release_hash(release)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this for backwards compatibility, because other fastlane plugins might rely on camelCase instead of snake_case?

Or is this temporary until you update the rest of the code?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For backwards compatibility

{
name: release.name,
releaseNotes: map_release_notes_hash(release.release_notes),
displayVersion: release.display_version,
buildVersion: release.build_version,
binaryDownloadUri: release.binary_download_uri,
firebaseConsoleUri: release.firebase_console_uri,
testingUri: release.testing_uri,
createTime: release.create_time
}
end

def self.map_release_notes_hash(release_notes)
return nil if release_notes.nil?

{ text: release_notes.text }
end

#####################################################
# @!group Documentation
#####################################################
Expand Down Expand Up @@ -106,6 +140,9 @@ def self.sample_return_value
},
displayVersion: "1.2.3",
buildVersion: "10",
binaryDownloadUri: "<URI>",
firebaseConsoleUri: "<URI>",
testingUri: "<URI>",
createTime: "2021-10-06T15:01:23Z"
}
end
Expand Down
@@ -1,4 +1,6 @@
require 'googleauth'
require 'fastlane_core/ui/ui'

module Fastlane
UI = FastlaneCore::UI unless Fastlane.const_defined?("UI")
module Auth
Expand All @@ -13,8 +15,8 @@ module FirebaseAppDistributionAuthClient
CLIENT_ID = "563584335869-fgrhgmd47bqnekij5i8b5pr03ho849e6.apps.googleusercontent.com"
CLIENT_SECRET = "j9iVZfS8kkCEFUPaAeJV0sAi"

# Returns the auth token for any of the auth methods (Firebase CLI token,
# Google service account, firebase-tools). To ensure that a specific
# Returns an authorization object for any of the auth methods (Firebase CLI token,
# Application Default Credentials, firebase-tools). To ensure that a specific
# auth method is used, unset all other auth variables/parameters to nil/empty
#
# args
Expand All @@ -23,43 +25,45 @@ module FirebaseAppDistributionAuthClient
# debug - Whether to enable debug-level logging
#
# env variables
# GOOGLE_APPLICATION_CREDENTIALS - see google_service_path
# FIREBASE_TOKEN - see firebase_cli_token
#
# Crashes if given invalid or missing credentials
def fetch_auth_token(google_service_path, firebase_cli_token, debug = false)
def get_authorization(google_service_path, firebase_cli_token, debug = false)
if !google_service_path.nil? && !google_service_path.empty?
UI.message("🔐 Authenticating with --service_credentials_file path parameter: #{google_service_path}")
token = service_account(google_service_path, debug)
service_account(google_service_path, debug)
elsif !firebase_cli_token.nil? && !firebase_cli_token.empty?
UI.message("🔐 Authenticating with --firebase_cli_token parameter")
token = firebase_token(firebase_cli_token, debug)
firebase_token(firebase_cli_token, debug)
elsif !ENV["FIREBASE_TOKEN"].nil? && !ENV["FIREBASE_TOKEN"].empty?
UI.message("🔐 Authenticating with FIREBASE_TOKEN environment variable")
token = firebase_token(ENV["FIREBASE_TOKEN"], debug)
elsif !ENV["GOOGLE_APPLICATION_CREDENTIALS"].nil? && !ENV["GOOGLE_APPLICATION_CREDENTIALS"].empty?
UI.message("🔐 Authenticating with GOOGLE_APPLICATION_CREDENTIALS environment variable: #{ENV['GOOGLE_APPLICATION_CREDENTIALS']}")
token = service_account(ENV["GOOGLE_APPLICATION_CREDENTIALS"], debug)
firebase_token(ENV["FIREBASE_TOKEN"], debug)
elsif !application_default_creds.nil?
UI.message("🔐 Authenticating with Application Default Credentials")
application_default_creds
elsif (refresh_token = refresh_token_from_firebase_tools)
UI.message("🔐 No authentication method specified. Using cached Firebase CLI credentials.")
token = firebase_token(refresh_token, debug)
UI.message("🔐 No authentication method found. Using cached Firebase CLI credentials.")
firebase_token(refresh_token, debug)
else
UI.user_error!(ErrorMessage::MISSING_CREDENTIALS)
nil
end
token
end

private

def application_default_creds
Google::Auth.get_application_default([SCOPE])
rescue
nil
end

def refresh_token_from_firebase_tools
config_path = format_config_path
if File.exist?(config_path)
begin
firebase_tools_tokens = JSON.parse(File.read(config_path))['tokens']
if firebase_tools_tokens.nil?
UI.user_error!(ErrorMessage::EMPTY_TOKENS_FIELD)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is this removed?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I might move this to a separate PR. I realized that this isn't actually an exceptional case. If they log out of the Firebase CLI (at least the version I'm using), that file will still exist it just won't have a tokens field. In that case we just want to return nil and continue.

return
end
return if firebase_tools_tokens.nil?
refresh_token = firebase_tools_tokens['refresh_token']
rescue JSON::ParserError
UI.user_error!(ErrorMessage::PARSE_FIREBASE_TOOLS_JSON_ERROR)
Expand All @@ -84,7 +88,7 @@ def firebase_token(refresh_token, debug)
refresh_token: refresh_token
)
client.fetch_access_token!
client.access_token
client
rescue Signet::AuthorizationError => error
error_message = ErrorMessage::REFRESH_TOKEN_ERROR
if debug
Expand All @@ -101,7 +105,8 @@ def service_account(google_service_path, debug)
json_key_io: File.open(google_service_path),
scope: SCOPE
)
service_account_credentials.fetch_access_token!["access_token"]
service_account_credentials.fetch_access_token!
service_account_credentials
rescue Errno::ENOENT
UI.user_error!("#{ErrorMessage::SERVICE_CREDENTIALS_NOT_FOUND}: #{google_service_path}")
rescue Signet::AuthorizationError => error
Expand Down
@@ -1,5 +1,5 @@
module ErrorMessage
MISSING_CREDENTIALS = "Missing authentication credentials. Check that your Firebase refresh token is set or that your service account file path is correct and try again."
MISSING_CREDENTIALS = "Missing authentication credentials. Set up Application Default Credentials, your Firebase refresh token, or sign in with the Firebase CLI, and try again."
MISSING_APP_ID = "Missing app id. Please check that the app parameter is set and try again"
SERVICE_CREDENTIALS_NOT_FOUND = "Service credentials file does not exist. Please check the service credentials path and try again"
PARSE_SERVICE_CREDENTIALS_ERROR = "Failed to extract service account information from the service credentials file"
Expand All @@ -23,7 +23,6 @@ module ErrorMessage
PLAY_IAS_TERMS_NOT_ACCEPTED = "You must accept the Play Internal App Sharing (IAS) terms to upload AABs."
INVALID_EMAIL_ADDRESS = "You passed an invalid email address."
TESTER_LIMIT_VIOLATION = "Creating testers would exceed tester limit"
EMPTY_TOKENS_FIELD = "Unable to find \"tokens\" field in the firebase-tools.json file. Ensure that the file has a tokens field and try again"

def self.aab_upload_error(aab_state)
"Failed to process the AAB: #{aab_state}"
Expand Down
67 changes: 0 additions & 67 deletions spec/firebase_app_distribution_api_client_spec.rb
Expand Up @@ -391,73 +391,6 @@
end
end

describe '#list_releases' do
let(:release1) do
{
name: "#{app_name}/releases/2c3d40a1b",
releaseNotes: {
text: "Here are some release notes!"
},
displayVersion: "1.2.2",
buildVersion: "9",
createTime: "2021-10-05T15:01:23Z"
}
end
let(:release2) do
{
name: "#{app_name}/releases/0a1b2c3d4",
releaseNotes: {
text: "Here are some newer release notes!"
},
displayVersion: "1.2.3",
buildVersion: "10",
createTime: "2021-10-06T15:01:23Z"
}
end
let(:list_releases_headers) do
{
'Authorization' => 'Bearer auth_token',
'X-Client-Version' => "fastlane/#{Fastlane::FirebaseAppDistribution::VERSION}"
}
end

it 'sets the page size and returns the response body' do
stubs.get("/v1/#{app_name}/releases?pageSize=2", list_releases_headers) do |env|
[
200,
{}, # response headers
{ releases: [release2, release1] }
]
end
result = api_client.list_releases(app_name, 2)
expect(result).to eq({ releases: [release2, release1] })
end

it 'sets the page size and token and returns the response body' do
stubs.get("/v1/#{app_name}/releases?pageSize=2&pageToken=the-token", list_releases_headers) do |env|
[
200,
{}, # response headers
{ releases: [release2, release1] }
]
end
result = api_client.list_releases(app_name, 2, 'the-token')
expect(result).to eq({ releases: [release2, release1] })
end

it 'fails and prints correct error message for a 404' do
stubs.get("/v1/#{app_name}/releases?pageSize=2", list_releases_headers) do |env|
[
404,
{}, # response headers
{}
]
end
expect { api_client.list_releases(app_name, 2) }
.to raise_error("#{ErrorMessage::INVALID_APP_ID}: #{app_name}")
end
end

describe '#distribute' do
it 'posts successfuly when tester emails and groupIds are defined' do
payload = { testerEmails: ["testers"], groupAliases: ["groups"] }
Expand Down