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.
@@ -4,6 +4,14 @@ module Fastlane
4
4
  module Auth
5
5
  module FirebaseAppDistributionAuthClient
6
6
  TOKEN_CREDENTIAL_URI = "https://oauth2.googleapis.com/token"
7
+ REDACTION_EXPOSED_LENGTH = 5
8
+ REDACTION_CHARACTER = "X"
9
+ SCOPE = "https://www.googleapis.com/auth/cloud-platform"
10
+
11
+ # In this type of application, the client secret is not treated as a secret.
12
+ # See: https://developers.google.com/identity/protocols/OAuth2InstalledApp
13
+ CLIENT_ID = "563584335869-fgrhgmd47bqnekij5i8b5pr03ho849e6.apps.googleusercontent.com"
14
+ CLIENT_SECRET = "j9iVZfS8kkCEFUPaAeJV0sAi"
7
15
 
8
16
  # Returns the auth token for any of the auth methods (Firebase CLI token,
9
17
  # Google service account, firebase-tools). To ensure that a specific
@@ -11,80 +19,118 @@ module Fastlane
11
19
  #
12
20
  # args
13
21
  # google_service_path - Absolute path to the Google service account file
14
- # firebase_cli_token - Firebase CLI refresh token from login action or
15
- # CI environment
22
+ # firebase_cli_token - Refresh token
23
+ # debug - Whether to enable debug-level logging
16
24
  #
17
25
  # env variables
18
26
  # GOOGLE_APPLICATION_CREDENTIALS - see google_service_path
19
27
  # FIREBASE_TOKEN - see firebase_cli_token
20
28
  #
21
29
  # Crashes if given invalid or missing credentials
22
- def fetch_auth_token(google_service_path, firebase_cli_token)
30
+ def fetch_auth_token(google_service_path, firebase_cli_token, debug = false)
23
31
  if !google_service_path.nil? && !google_service_path.empty?
24
- UI.message("Authenticating with --service_credentials_file path parameter: #{google_service_path}")
25
- token = service_account(google_service_path)
32
+ UI.message("🔐 Authenticating with --service_credentials_file path parameter: #{google_service_path}")
33
+ token = service_account(google_service_path, debug)
26
34
  elsif !firebase_cli_token.nil? && !firebase_cli_token.empty?
27
- UI.message("Authenticating with --firebase_cli_token parameter")
28
- token = firebase_token(firebase_cli_token)
35
+ UI.message("🔐 Authenticating with --firebase_cli_token parameter")
36
+ token = firebase_token(firebase_cli_token, debug)
29
37
  elsif !ENV["FIREBASE_TOKEN"].nil? && !ENV["FIREBASE_TOKEN"].empty?
30
- UI.message("Authenticating with FIREBASE_TOKEN environment variable")
31
- token = firebase_token(ENV["FIREBASE_TOKEN"])
38
+ UI.message("🔐 Authenticating with FIREBASE_TOKEN environment variable")
39
+ token = firebase_token(ENV["FIREBASE_TOKEN"], debug)
32
40
  elsif !ENV["GOOGLE_APPLICATION_CREDENTIALS"].nil? && !ENV["GOOGLE_APPLICATION_CREDENTIALS"].empty?
33
- UI.message("Authenticating with GOOGLE_APPLICATION_CREDENTIALS environment variable: #{ENV['GOOGLE_APPLICATION_CREDENTIALS']}")
34
- token = service_account(ENV["GOOGLE_APPLICATION_CREDENTIALS"])
41
+ UI.message("🔐 Authenticating with GOOGLE_APPLICATION_CREDENTIALS environment variable: #{ENV['GOOGLE_APPLICATION_CREDENTIALS']}")
42
+ token = service_account(ENV["GOOGLE_APPLICATION_CREDENTIALS"], debug)
35
43
  elsif (refresh_token = refresh_token_from_firebase_tools)
36
- UI.message("No authentication method specified. Using cached Firebase CLI credentials.")
37
- token = firebase_token(refresh_token)
44
+ UI.message("🔐 No authentication method specified. Using cached Firebase CLI credentials.")
45
+ token = firebase_token(refresh_token, debug)
38
46
  else
39
47
  UI.user_error!(ErrorMessage::MISSING_CREDENTIALS)
40
48
  end
41
- UI.success("🔐 Authenticated successfully.")
42
49
  token
43
50
  end
44
51
 
45
52
  private
46
53
 
47
54
  def refresh_token_from_firebase_tools
48
- if ENV["XDG_CONFIG_HOME"].nil? || ENV["XDG_CONFIG_HOME"].empty?
49
- config_path = File.expand_path(".config/configstore/firebase-tools.json", "~")
50
- else
51
- config_path = File.expand_path("configstore/firebase-tools.json", ENV["XDG_CONFIG_HOME"])
52
- end
53
-
55
+ config_path = format_config_path
54
56
  if File.exist?(config_path)
55
57
  begin
56
- refresh_token = JSON.parse(File.read(config_path))['tokens']['refresh_token']
57
- refresh_token unless refresh_token.nil? || refresh_token.empty?
58
- # TODO: Catch parser errors, improve error handling here
59
- # Returns nil when there is an empty "tokens" field in the firebase-tools json
60
- rescue NoMethodError
58
+ firebase_tools_tokens = JSON.parse(File.read(config_path))['tokens']
59
+ if firebase_tools_tokens.nil?
60
+ UI.user_error!(ErrorMessage::EMPTY_TOKENS_FIELD)
61
+ return
62
+ end
63
+ refresh_token = firebase_tools_tokens['refresh_token']
64
+ rescue JSON::ParserError
65
+ UI.user_error!(ErrorMessage::PARSE_FIREBASE_TOOLS_JSON_ERROR)
61
66
  end
67
+ refresh_token unless refresh_token.nil? || refresh_token.empty?
68
+ end
69
+ end
70
+
71
+ def format_config_path
72
+ if ENV["XDG_CONFIG_HOME"].nil? || ENV["XDG_CONFIG_HOME"].empty?
73
+ File.expand_path(".config/configstore/firebase-tools.json", "~")
74
+ else
75
+ File.expand_path("configstore/firebase-tools.json", ENV["XDG_CONFIG_HOME"])
62
76
  end
63
77
  end
64
78
 
65
- def firebase_token(refresh_token)
79
+ def firebase_token(refresh_token, debug)
66
80
  client = Signet::OAuth2::Client.new(
67
81
  token_credential_uri: TOKEN_CREDENTIAL_URI,
68
- client_id: Fastlane::Actions::FirebaseAppDistributionLoginAction::CLIENT_ID,
69
- client_secret: Fastlane::Actions::FirebaseAppDistributionLoginAction::CLIENT_SECRET,
82
+ client_id: CLIENT_ID,
83
+ client_secret: CLIENT_SECRET,
70
84
  refresh_token: refresh_token
71
85
  )
72
86
  client.fetch_access_token!
73
87
  client.access_token
74
- rescue Signet::AuthorizationError
75
- UI.user_error!(ErrorMessage::REFRESH_TOKEN_ERROR)
88
+ rescue Signet::AuthorizationError => error
89
+ error_message = ErrorMessage::REFRESH_TOKEN_ERROR
90
+ if debug
91
+ error_message += "\nRefresh token used: #{format_token(refresh_token)}\n"
92
+ error_message += error_details(error)
93
+ else
94
+ error_message += " #{debug_instructions}"
95
+ end
96
+ UI.user_error!(error_message)
76
97
  end
77
98
 
78
- def service_account(google_service_path)
99
+ def service_account(google_service_path, debug)
79
100
  service_account_credentials = Google::Auth::ServiceAccountCredentials.make_creds(
80
101
  json_key_io: File.open(google_service_path),
81
- scope: Fastlane::Actions::FirebaseAppDistributionLoginAction::SCOPE
102
+ scope: SCOPE
82
103
  )
83
104
  service_account_credentials.fetch_access_token!["access_token"]
84
105
  rescue Errno::ENOENT
85
106
  UI.user_error!("#{ErrorMessage::SERVICE_CREDENTIALS_NOT_FOUND}: #{google_service_path}")
86
- rescue Signet::AuthorizationError
87
- UI.user_error!("#{ErrorMessage::SERVICE_CREDENTIALS_ERROR}: #{google_service_path}")
107
+ rescue Signet::AuthorizationError => error
108
+ error_message = "#{ErrorMessage::SERVICE_CREDENTIALS_ERROR}: \"#{google_service_path}\""
109
+ if debug
110
+ error_message += "\n#{error_details(error)}"
111
+ else
112
+ error_message += ". #{debug_instructions}"
113
+ end
114
+ UI.user_error!(error_message)
115
+ end
116
+
117
+ def error_details(error)
118
+ "#{error.message}\nResponse status: #{error.response.status}"
119
+ end
120
+
121
+ def debug_instructions
122
+ "For more information, try again with firebase_app_distribution's \"debug\" parameter set to \"true\"."
123
+ end
124
+
125
+ # Formats and redacts a token for printing out during debug logging. Examples:
126
+ # 'abcd' -> '"abcd"''
127
+ # 'abcdef1234' -> '"XXXXXf1234" (redacted)'
128
+ def format_token(str)
129
+ redaction_notice = str.length > REDACTION_EXPOSED_LENGTH ? " (redacted)" : ""
130
+ exposed_start_char = [str.length - REDACTION_EXPOSED_LENGTH, 0].max
131
+ exposed_characters = str[exposed_start_char, REDACTION_EXPOSED_LENGTH]
132
+ redacted_characters = REDACTION_CHARACTER * [str.length - REDACTION_EXPOSED_LENGTH, 0].max
133
+ "\"#{redacted_characters}#{exposed_characters}\"#{redaction_notice}"
88
134
  end
89
135
  end
90
136
  end
@@ -3,19 +3,29 @@ module ErrorMessage
3
3
  MISSING_APP_ID = "Missing app id. Please check that the app parameter is set and try again"
4
4
  SERVICE_CREDENTIALS_NOT_FOUND = "Service credentials file does not exist. Please check the service credentials path and try again"
5
5
  PARSE_SERVICE_CREDENTIALS_ERROR = "Failed to extract service account information from the service credentials file"
6
+ PARSE_FIREBASE_TOOLS_JSON_ERROR = "Encountered error parsing json file. Ensure the firebase-tools.json file is formatted correctly"
6
7
  UPLOAD_RELEASE_NOTES_ERROR = "App Distribution halted because it had a problem uploading release notes"
7
8
  UPLOAD_TESTERS_ERROR = "App Distribution halted because it had a problem adding testers/groups"
8
9
  GET_RELEASE_TIMEOUT = "App Distribution failed to fetch release information"
9
10
  REFRESH_TOKEN_ERROR = "App Distribution could not generate credentials from the refresh token specified."
10
- GET_APP_ERROR = "App Distribution failed to fetch app information"
11
11
  APP_NOT_ONBOARDED_ERROR = "App Distribution not onboarded"
12
- GET_APP_NO_CONTACT_EMAIL_ERROR = "App Distribution could not find a contact email associated with this app. Contact Email"
13
12
  INVALID_APP_ID = "App Distribution could not find your app. Make sure to onboard your app by pressing the \"Get started\" button on the App Distribution page in the Firebase console: https://console.firebase.google.com/project/_/appdistribution. App ID"
13
+ INVALID_PROJECT = "App Distribution could not find your Firebase project. Make sure to onboard an app in your project by pressing the \"Get started\" button on the App Distribution page in the Firebase console: https://console.firebase.google.com/project/_/appdistribution."
14
14
  INVALID_PATH = "Could not read content from"
15
- INVALID_TESTERS = "Could not enable access for testers. Check that the groups exist and the tester emails are formatted correctly"
16
- INVALID_RELEASE_ID = "App distribution failed to fetch release with id"
15
+ INVALID_TESTERS = "Could not enable access for testers. Check that the tester emails are formatted correctly, the groups exist and you are using group aliases (not group names) for specifying groups."
17
16
  INVALID_RELEASE_NOTES = "Failed to add release notes"
18
- SERVICE_CREDENTIALS_ERROR = "App Distribution could not generate credentials from the service credentials file specified. Service Account Path"
17
+ SERVICE_CREDENTIALS_ERROR = "App Distribution could not generate credentials from the service credentials file specified"
18
+ PLAY_ACCOUNT_NOT_LINKED = "This project is not linked to a Google Play account."
19
+ APP_NOT_PUBLISHED = "This app is not published in the Google Play console."
20
+ NO_APP_WITH_GIVEN_BUNDLE_ID_IN_PLAY_ACCOUNT = "App with matching package name does not exist in Google Play."
21
+ PLAY_IAS_TERMS_NOT_ACCEPTED = "You must accept the Play Internal App Sharing (IAS) terms to upload AABs."
22
+ INVALID_EMAIL_ADDRESS = "You passed an invalid email address."
23
+ TESTER_LIMIT_VIOLATION = "Creating testers would exceed tester limit"
24
+ 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"
25
+
26
+ def self.aab_upload_error(aab_state)
27
+ "Failed to process the AAB: #{aab_state}"
28
+ end
19
29
 
20
30
  def self.binary_not_found(binary_type)
21
31
  "Could not find the #{binary_type}. Make sure you set the #{binary_type} path parameter to point to your #{binary_type}"
@@ -4,6 +4,15 @@ module Fastlane
4
4
  UI = FastlaneCore::UI unless Fastlane.const_defined?("UI")
5
5
  module Helper
6
6
  module FirebaseAppDistributionHelper
7
+ def binary_type_from_path(binary_path)
8
+ extension = File.extname(binary_path)
9
+ return :APK if extension == '.apk'
10
+ return :AAB if extension == '.aab'
11
+ return :IPA if extension == '.ipa'
12
+
13
+ UI.user_error!("Unsupported distribution file format, should be .ipa, .apk or .aab")
14
+ end
15
+
7
16
  def get_value_from_value_or_file(value, path)
8
17
  if (value.nil? || value.empty?) && !path.nil?
9
18
  begin
@@ -15,13 +24,12 @@ module Fastlane
15
24
  value
16
25
  end
17
26
 
18
- # Returns the array representation of a string with comma seperated values.
19
- #
20
- # Does not work with strings whose individual values have spaces. EX "Hello World" the space will be removed to "HelloWorld"
27
+ # Returns the array representation of a string with trimmed comma
28
+ # seperated values.
21
29
  def string_to_array(string)
22
30
  return nil if string.nil? || string.empty?
23
- string_array = string.gsub(/\s+/, '').split(",")
24
- return string_array
31
+ # Strip string and then strip individual values
32
+ string.strip.split(",").map(&:strip)
25
33
  end
26
34
 
27
35
  def parse_plist(path)
@@ -35,6 +43,19 @@ module Fastlane
35
43
  UI.shell_error!("can't extract GOOGLE_APP_ID") if identifier.empty?
36
44
  return identifier
37
45
  end
46
+
47
+ def blank?(value)
48
+ # Taken from https://apidock.com/rails/Object/blank%3F
49
+ value.respond_to?(:empty?) ? value.empty? : !value
50
+ end
51
+
52
+ def present?(value)
53
+ !blank?(value)
54
+ end
55
+
56
+ def app_name_from_app_id(app_id)
57
+ "projects/#{app_id.split(':')[1]}/apps/#{app_id}"
58
+ end
38
59
  end
39
60
  end
40
61
  end
@@ -3,35 +3,75 @@ class UploadStatusResponse
3
3
  @response_json_hash = response_json_hash
4
4
  end
5
5
 
6
+ def done
7
+ !!@response_json_hash[:done]
8
+ end
9
+
10
+ def response
11
+ @response_json_hash[:response]
12
+ end
13
+
14
+ def release
15
+ response ? response[:release] : nil
16
+ end
17
+
18
+ def release_name
19
+ release ? release[:name] : nil
20
+ end
21
+
22
+ def release_version
23
+ if release
24
+ if release[:displayVersion] && release[:buildVersion]
25
+ "#{release[:displayVersion]} (#{release[:buildVersion]})"
26
+ elsif release[:displayVersion]
27
+ release[:displayVersion]
28
+ else
29
+ release[:buildVersion]
30
+ end
31
+ end
32
+ end
33
+
34
+ def firebase_console_uri
35
+ release ? release[:firebaseConsoleUri] : nil
36
+ end
37
+
38
+ def testing_uri
39
+ release ? release[:testingUri] : nil
40
+ end
41
+
42
+ def binary_download_uri
43
+ release ? release[:binaryDownloadUri] : nil
44
+ end
45
+
6
46
  def status
7
- @response_json_hash[:status]
47
+ response ? response[:result] : nil
8
48
  end
9
49
 
10
- def success?
11
- status == 'SUCCESS'
50
+ def error
51
+ @response_json_hash[:error]
12
52
  end
13
53
 
14
- def in_progress?
15
- status == 'IN_PROGRESS'
54
+ def error_message
55
+ error ? error[:message] : nil
16
56
  end
17
57
 
18
- def error?
19
- status == 'ERROR'
58
+ def success?
59
+ done && !!release
20
60
  end
21
61
 
22
- def already_uploaded?
23
- status == 'ALREADY_UPLOADED'
62
+ def in_progress?
63
+ !done
24
64
  end
25
65
 
26
- def release_hash
27
- @response_json_hash[:release]
66
+ def error?
67
+ done && message
28
68
  end
29
69
 
30
- def release_id
31
- release_hash ? release_hash[:id] : nil
70
+ def release_updated?
71
+ done && status == 'RELEASE_UPDATED'
32
72
  end
33
73
 
34
- def message
35
- @response_json_hash[:message]
74
+ def release_unmodified?
75
+ done && status == 'RELEASE_UNMODIFIED'
36
76
  end
37
77
  end
@@ -1,5 +1,5 @@
1
1
  module Fastlane
2
2
  module FirebaseAppDistribution
3
- VERSION = "0.2.5"
3
+ VERSION = "0.5.0"
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fastlane-plugin-firebase_app_distribution
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.5
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stefan Natchev
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2021-01-06 00:00:00.000000000 Z
13
+ date: 2023-02-15 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: pry
@@ -149,9 +149,14 @@ extra_rdoc_files: []
149
149
  files:
150
150
  - LICENSE
151
151
  - README.md
152
+ - fad-icon.png
152
153
  - lib/fastlane/plugin/firebase_app_distribution.rb
153
154
  - lib/fastlane/plugin/firebase_app_distribution/actions/firebase_app_distribution_action.rb
154
- - lib/fastlane/plugin/firebase_app_distribution/actions/firebase_app_distribution_login.rb
155
+ - lib/fastlane/plugin/firebase_app_distribution/actions/firebase_app_distribution_add_testers_action.rb
156
+ - lib/fastlane/plugin/firebase_app_distribution/actions/firebase_app_distribution_get_latest_release.rb
157
+ - lib/fastlane/plugin/firebase_app_distribution/actions/firebase_app_distribution_get_udids.rb
158
+ - lib/fastlane/plugin/firebase_app_distribution/actions/firebase_app_distribution_remove_testers_action.rb
159
+ - lib/fastlane/plugin/firebase_app_distribution/client/aab_info.rb
155
160
  - lib/fastlane/plugin/firebase_app_distribution/client/error_response.rb
156
161
  - lib/fastlane/plugin/firebase_app_distribution/client/firebase_app_distribution_api_client.rb
157
162
  - lib/fastlane/plugin/firebase_app_distribution/helper/firebase_app_distribution_auth_client.rb
@@ -178,7 +183,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
178
183
  - !ruby/object:Gem::Version
179
184
  version: '0'
180
185
  requirements: []
181
- rubygems_version: 3.1.4
186
+ rubygems_version: 3.4.6
182
187
  signing_key:
183
188
  specification_version: 4
184
189
  summary: Release your beta builds to Firebase App Distribution. https://firebase.google.com/docs/app-distribution
@@ -1,58 +0,0 @@
1
- require 'googleauth'
2
- require 'googleauth/stores/file_token_store'
3
- require "google/apis/people_v1"
4
- require "fileutils"
5
-
6
- module Fastlane
7
- module Actions
8
- class FirebaseAppDistributionLoginAction < Action
9
- OOB_URI = "urn:ietf:wg:oauth:2.0:oob"
10
- SCOPE = "https://www.googleapis.com/auth/cloud-platform"
11
- CLIENT_ID = "563584335869-fgrhgmd47bqnekij5i8b5pr03ho849e6.apps.googleusercontent.com"
12
- CLIENT_SECRET = "j9iVZfS8kkCEFUPaAeJV0sAi"
13
-
14
- def self.run(params)
15
- client_id = Google::Auth::ClientId.new(CLIENT_ID, CLIENT_SECRET)
16
- authorizer = Google::Auth::UserAuthorizer.new(client_id, SCOPE, nil)
17
- url = authorizer.get_authorization_url(base_url: OOB_URI)
18
-
19
- UI.message("Open the following address in your browser and sign in with your Google account:")
20
- UI.message(url)
21
- UI.message("")
22
- code = UI.input("Enter the resulting code here: ")
23
- credentials = authorizer.get_credentials_from_code(code: code, base_url: OOB_URI)
24
- UI.message("")
25
-
26
- UI.success("Set the refresh token as the FIREBASE_TOKEN environment variable")
27
- UI.success("Refresh Token: #{credentials.refresh_token}")
28
- rescue Signet::AuthorizationError
29
- UI.error("The code you entered is invalid. Copy and paste the code and try again.")
30
- rescue => error
31
- UI.error(error.to_s)
32
- UI.crash!("An error has occured, please login again.")
33
- end
34
-
35
- #####################################################
36
- # @!group Documentation
37
- #####################################################
38
-
39
- def self.description
40
- "Authenticate with Firebase App Distribution using a Google account."
41
- end
42
-
43
- def self.details
44
- "Log in to Firebase App Distribution using a Google account to generate an authentication "\
45
- "token. This token is stored within an environment variable and used to authenticate with your Firebase project. "\
46
- "See https://firebase.google.com/docs/app-distribution/ios/distribute-fastlane for more information."
47
- end
48
-
49
- def self.authors
50
- ["Manny Jimenez Github: mannyjimenez0810, Alonso Salas Infante Github: alonsosalasinfante"]
51
- end
52
-
53
- def self.is_supported?(platform)
54
- [:ios, :android].include?(platform)
55
- end
56
- end
57
- end
58
- end