fastlane-plugin-firebase_app_distribution 0.1.4 → 0.2.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 727032daa6617cc4ff637760345d66514b0a3f798653cc8b5f05c043e74df730
4
- data.tar.gz: 2981fccbd5bcc75775cc8ffd34e017dddc724346466c8e5dfeeed53c577609aa
3
+ metadata.gz: 5c1f9a3bb79927dc10b2ed68e66ad0c8b9593ef5a31a6cc50d91ed635eb6443a
4
+ data.tar.gz: bfadb919bccb7bb7b1b7d943cd92b0709f3f8d432e8508efc2ac56bb8969c809
5
5
  SHA512:
6
- metadata.gz: 3f94539379bfca995ed56795ba992905b5f7259c70becaf410e7695bd0a2a6de9ed75a3c4cf158be14fa153678868d026b928772c389feba05e715412b6689a1
7
- data.tar.gz: fd084df5275b9cf21684d3ad14030de4c8c85954457fcb18ca272577dcf07e66875854b1daf2c712042e3ebde363c8240cc648b14b16dfc08d85cf2530851463
6
+ metadata.gz: 55f7b6245940db7d3141ac707d52646d5dc369d7a9d4a2add36e1e5efe11365e0ef3aaa13cbab59cb51f8696320d186570bd5c3e38f5856fc8a801246b3f93da
7
+ data.tar.gz: dc02fa663afe0cfeba8b71f2de2bc91feed18cf2ca3b0134567d80729d529c9dab9573831e0c23100e96f00c2447b1af95dc688860b514ea6080fd3e303ed438
@@ -1,39 +1,56 @@
1
- require 'tempfile'
2
1
  require 'fastlane/action'
3
2
  require 'open3'
4
3
  require 'shellwords'
4
+ require 'googleauth'
5
+ require_relative '../helper/upload_status_response'
5
6
  require_relative '../helper/firebase_app_distribution_helper'
7
+ require_relative '../helper/firebase_app_distribution_error_message'
8
+ require_relative '../client/firebase_app_distribution_api_client'
9
+ require_relative '../helper/firebase_app_distribution_auth_client'
6
10
 
7
11
  ## TODO: should always use a file underneath? I think so.
8
12
  ## How should we document the usage of release notes?
9
13
  module Fastlane
10
14
  module Actions
11
15
  class FirebaseAppDistributionAction < Action
12
- DEFAULT_FIREBASE_CLI_PATH = `which firebase`
13
16
  FIREBASECMD_ACTION = "appdistribution:distribute".freeze
14
17
 
18
+ extend Auth::FirebaseAppDistributionAuthClient
15
19
  extend Helper::FirebaseAppDistributionHelper
16
20
 
17
21
  def self.run(params)
18
22
  params.values # to validate all inputs before looking for the ipa/apk
19
- cmd = [Shellwords.escape(params[:firebase_cli_path].chomp), FIREBASECMD_ACTION]
20
- cmd << Shellwords.escape(params[:ipa_path] || params[:apk_path])
21
- cmd << "--app #{params[:app]}"
22
-
23
- cmd << groups_flag(params)
24
- cmd << testers_flag(params)
25
- cmd << release_notes_flag(params)
26
- cmd << flag_value_if_supplied('--token', :firebase_cli_token, params)
27
- cmd << flag_if_supplied('--debug', :debug, params)
28
-
29
- Actions.sh_control_output(
30
- cmd.compact.join(" "),
31
- print_command: false,
32
- print_command_output: true
33
- )
34
- # make sure we do this, even in the case of an error.
35
- ensure
36
- cleanup_tempfiles
23
+ auth_token = fetch_auth_token(params[:service_credentials_file], params[:firebase_cli_token])
24
+ binary_path = params[:ipa_path] || params[:apk_path]
25
+ platform = lane_platform || platform_from_path(binary_path)
26
+ fad_api_client = Client::FirebaseAppDistributionApiClient.new(auth_token, platform)
27
+
28
+ if params[:app] # Set app_id if it is specified as a parameter
29
+ app_id = params[:app]
30
+ elsif platform == :ios
31
+ archive_path = Actions.lane_context[SharedValues::XCODEBUILD_ARCHIVE]
32
+ if archive_path
33
+ app_id = get_ios_app_id_from_archive(archive_path)
34
+ end
35
+ end
36
+
37
+ if app_id.nil?
38
+ UI.crash!(ErrorMessage::MISSING_APP_ID)
39
+ end
40
+ release_id = fad_api_client.upload(app_id, binary_path, platform.to_s)
41
+ if release_id.nil?
42
+ return
43
+ end
44
+
45
+ release_notes = get_value_from_value_or_file(params[:release_notes], params[:release_notes_file])
46
+ fad_api_client.post_notes(app_id, release_id, release_notes)
47
+
48
+ testers = get_value_from_value_or_file(params[:testers], params[:testers_file])
49
+ groups = get_value_from_value_or_file(params[:groups], params[:groups_file])
50
+ emails = string_to_array(testers)
51
+ group_ids = string_to_array(groups)
52
+ fad_api_client.enable_access(app_id, release_id, emails, group_ids)
53
+ UI.success("🎉 App Distribution upload finished successfully.")
37
54
  end
38
55
 
39
56
  def self.description
@@ -41,7 +58,7 @@ module Fastlane
41
58
  end
42
59
 
43
60
  def self.authors
44
- ["Stefan Natchev"]
61
+ ["Stefan Natchev", "Manny Jimenez Github: mannyjimenez0810, Alonso Salas Infante Github: alonsosalasinfante"]
45
62
  end
46
63
 
47
64
  # supports markdown.
@@ -49,14 +66,26 @@ module Fastlane
49
66
  "Release your beta builds with Firebase App Distribution"
50
67
  end
51
68
 
52
- def self.available_options
53
- platform = Actions.lane_context[Actions::SharedValues::PLATFORM_NAME]
69
+ def self.lane_platform
70
+ Actions.lane_context[Actions::SharedValues::PLATFORM_NAME]
71
+ end
72
+
73
+ def self.platform_from_path(binary_path)
74
+ return nil unless binary_path
75
+ case binary_path.split('.').last
76
+ when 'ipa'
77
+ :ios
78
+ when 'apk'
79
+ :android
80
+ end
81
+ end
54
82
 
55
- if platform == :ios || platform.nil?
83
+ def self.available_options
84
+ if lane_platform == :ios || lane_platform.nil?
56
85
  ipa_path_default = Dir["*.ipa"].sort_by { |x| File.mtime(x) }.last
57
86
  end
58
87
 
59
- if platform == :android
88
+ if lane_platform == :android
60
89
  apk_path_default = Dir["*.apk"].last || Dir[File.join("app", "build", "outputs", "apk", "app-release.apk")].last
61
90
  end
62
91
 
@@ -84,25 +113,13 @@ module Fastlane
84
113
  FastlaneCore::ConfigItem.new(key: :app,
85
114
  env_name: "FIREBASEAPPDISTRO_APP",
86
115
  description: "Your app's Firebase App ID. You can find the App ID in the Firebase console, on the General Settings page",
87
- optional: false,
116
+ optional: true,
88
117
  type: String),
89
118
  FastlaneCore::ConfigItem.new(key: :firebase_cli_path,
119
+ deprecated: "This plugin no longer uses the Firebase CLI",
90
120
  env_name: "FIREBASEAPPDISTRO_FIREBASE_CLI_PATH",
91
121
  description: "The absolute path of the firebase cli command",
92
- default_value: DEFAULT_FIREBASE_CLI_PATH,
93
- default_value_dynamic: true,
94
- optional: false,
95
- type: String,
96
- verify_block: proc do |value|
97
- value.chomp!
98
- if value.to_s == "" || !File.exist?(value)
99
- UI.user_error!("firebase_cli_path: missing path to firebase cli tool. Please install firebase in $PATH or specify path")
100
- end
101
-
102
- unless is_firebasecmd_supported?(value)
103
- UI.user_error!("firebase_cli_path: `#{value}` does not support the `#{FIREBASECMD_ACTION}` command. Please download (https://appdistro.page.link/firebase-cli-download) or specify the path to the correct version of firebse")
104
- end
105
- end),
122
+ type: String),
106
123
  FastlaneCore::ConfigItem.new(key: :groups,
107
124
  env_name: "FIREBASEAPPDISTRO_GROUPS",
108
125
  description: "The groups used for distribution, separated by commas",
@@ -143,7 +160,11 @@ module Fastlane
143
160
  description: "Print verbose debug output",
144
161
  optional: true,
145
162
  default_value: false,
146
- is_string: false)
163
+ is_string: false),
164
+ FastlaneCore::ConfigItem.new(key: :service_credentials_file,
165
+ description: "Path to Google service account json",
166
+ optional: true,
167
+ type: String)
147
168
  ]
148
169
  end
149
170
 
@@ -165,18 +186,6 @@ module Fastlane
165
186
  CODE
166
187
  ]
167
188
  end
168
-
169
- ## TODO: figure out if we can surpress color output.
170
- def self.is_firebasecmd_supported?(cmd)
171
- outerr, status = Open3.capture2e(cmd, "--non-interactive", FIREBASECMD_ACTION, "--help")
172
- return false unless status.success?
173
-
174
- if outerr =~ /is not a Firebase command/
175
- return false
176
- end
177
-
178
- true
179
- end
180
189
  end
181
190
  end
182
191
  end
@@ -0,0 +1,58 @@
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
@@ -0,0 +1,228 @@
1
+ require 'fastlane_core/ui/ui'
2
+ require_relative '../actions/firebase_app_distribution_login'
3
+
4
+ module Fastlane
5
+ module Client
6
+ class FirebaseAppDistributionApiClient
7
+ BASE_URL = "https://firebaseappdistribution.googleapis.com"
8
+ TOKEN_CREDENTIAL_URI = "https://oauth2.googleapis.com/token"
9
+ MAX_POLLING_RETRIES = 60
10
+ POLLING_INTERVAL_SECONDS = 2
11
+
12
+ def initialize(auth_token, platform)
13
+ @auth_token = auth_token
14
+
15
+ if platform.nil?
16
+ @binary_type = "IPA/APK"
17
+ elsif platform == :ios
18
+ @binary_type = "IPA"
19
+ else
20
+ @binary_type = "APK"
21
+ end
22
+ end
23
+
24
+ # Enables tester access to the specified app release. Skips this
25
+ # step if no testers are passed in (emails and group_ids are nil/empty).
26
+ #
27
+ # args
28
+ # app_id - Firebase App ID
29
+ # release_id - App release ID, returned by upload_status endpoint
30
+ # emails - String array of app testers' email addresses
31
+ # group_ids - String array of Firebase tester group IDs
32
+ #
33
+ # Throws a user_error if app_id, emails, or group_ids are invalid
34
+ def enable_access(app_id, release_id, emails, group_ids)
35
+ if (emails.nil? || emails.empty?) && (group_ids.nil? || group_ids.empty?)
36
+ UI.success("✅ No testers passed in. Skipping this step.")
37
+ return
38
+ end
39
+ payload = { emails: emails, groupIds: group_ids }
40
+ begin
41
+ connection.post(enable_access_url(app_id, release_id), payload.to_json) do |request|
42
+ request.headers["Authorization"] = "Bearer " + @auth_token
43
+ end
44
+ rescue Faraday::ResourceNotFound
45
+ UI.user_error!("#{ErrorMessage::INVALID_APP_ID}: #{app_id}")
46
+ rescue Faraday::ClientError
47
+ UI.user_error!("#{ErrorMessage::INVALID_TESTERS} \nEmails: #{emails} \nGroups: #{group_ids}")
48
+ end
49
+ UI.success("✅ Added testers/groups.")
50
+ end
51
+
52
+ # Posts notes for the specified app release. Skips this
53
+ # step if no notes are passed in (release_notes is nil/empty).
54
+ #
55
+ # args
56
+ # app_id - Firebase App ID
57
+ # release_id - App release ID, returned by upload_status endpoint
58
+ # release_notes - String of notes for this release
59
+ #
60
+ # Throws a user_error if app_id or release_id are invalid
61
+ def post_notes(app_id, release_id, release_notes)
62
+ payload = { releaseNotes: { releaseNotes: release_notes } }
63
+ if release_notes.nil? || release_notes.empty?
64
+ UI.success("✅ No release notes passed in. Skipping this step.")
65
+ return
66
+ end
67
+ begin
68
+ connection.post(release_notes_create_url(app_id, release_id), payload.to_json) do |request|
69
+ request.headers["Authorization"] = "Bearer " + @auth_token
70
+ end
71
+ rescue Faraday::ResourceNotFound
72
+ UI.user_error!("#{ErrorMessage::INVALID_APP_ID}: #{app_id}")
73
+ # rescue Faraday::ClientError
74
+ # UI.user_error!("#{ErrorMessage::INVALID_RELEASE_ID}: #{release_id}")
75
+ end
76
+ UI.success("✅ Posted release notes.")
77
+ end
78
+
79
+ # Returns the url encoded upload token used for get_upload_status calls:
80
+ # projects/<project-number>/apps/<app-id>/releases/-/binaries/<binary-hash>
81
+ #
82
+ # args
83
+ # app_id - Firebase App ID
84
+ # binary_path - Absolute path to your app's apk/ipa file
85
+ #
86
+ # Throws a user_error if an invalid app id is passed in, the binary file does
87
+ # not exist, or invalid auth credentials are used (e.g. wrong project permissions)
88
+ def get_upload_token(app_id, binary_path)
89
+ if binary_path.nil? || !File.exist?(binary_path)
90
+ UI.crash!("#{ErrorMessage.binary_not_found(@binary_type)}: #{binary_path}")
91
+ end
92
+ binary_hash = Digest::SHA256.hexdigest(File.open(binary_path).read)
93
+
94
+ begin
95
+ response = connection.get(v1_apps_url(app_id)) do |request|
96
+ request.headers["Authorization"] = "Bearer " + @auth_token
97
+ end
98
+ rescue Faraday::ResourceNotFound
99
+ UI.user_error!("#{ErrorMessage::INVALID_APP_ID}: #{app_id}")
100
+ end
101
+ contact_email = response.body[:contactEmail]
102
+ if contact_email.nil? || contact_email.strip.empty?
103
+ UI.user_error!(ErrorMessage::GET_APP_NO_CONTACT_EMAIL_ERROR)
104
+ end
105
+ return upload_token_format(response.body[:appId], response.body[:projectNumber], binary_hash)
106
+ end
107
+
108
+ # Uploads the app binary to the Firebase API
109
+ #
110
+ # args
111
+ # app_id - Firebase App ID
112
+ # binary_path - Absolute path to your app's apk/ipa file
113
+ # platform - 'android' or 'ios'
114
+ #
115
+ # Throws a user_error if an invalid app id is passed in, or if the binary file does not exist
116
+ def upload_binary(app_id, binary_path, platform)
117
+ connection.post(binary_upload_url(app_id), File.open(binary_path).read) do |request|
118
+ request.headers["Authorization"] = "Bearer " + @auth_token
119
+ request.headers["X-APP-DISTRO-API-CLIENT-ID"] = "fastlane"
120
+ request.headers["X-APP-DISTRO-API-CLIENT-TYPE"] = platform
121
+ request.headers["X-APP-DISTRO-API-CLIENT-VERSION"] = Fastlane::FirebaseAppDistribution::VERSION
122
+ end
123
+ rescue Faraday::ResourceNotFound
124
+ UI.user_error!("#{ErrorMessage::INVALID_APP_ID}: #{app_id}")
125
+ rescue Errno::ENOENT # Raised when binary_path file does not exist
126
+ UI.user_error!("#{ErrorMessage.binary_not_found(@binary_type)}: #{binary_path}")
127
+ end
128
+
129
+ # Uploads the binary file if it has not already been uploaded
130
+ # Takes at least POLLING_INTERVAL_SECONDS between polling get_upload_status
131
+ #
132
+ # args
133
+ # app_id - Firebase App ID
134
+ # binary_path - Absolute path to your app's apk/ipa file
135
+ #
136
+ # Returns the release_id on a successful release, otherwise returns nil.
137
+ #
138
+ # Throws a UI error if the number of polling retries exceeds MAX_POLLING_RETRIES
139
+ # Crashes if not able to upload the binary
140
+ def upload(app_id, binary_path, platform)
141
+ upload_token = get_upload_token(app_id, binary_path)
142
+ upload_status_response = get_upload_status(app_id, upload_token)
143
+ if upload_status_response.success? || upload_status_response.already_uploaded?
144
+ UI.success("✅ This #{@binary_type} has been uploaded before. Skipping upload step.")
145
+ else
146
+ unless upload_status_response.in_progress?
147
+ UI.message("⌛ Uploading the #{@binary_type}.")
148
+ upload_binary(app_id, binary_path, platform)
149
+ end
150
+ MAX_POLLING_RETRIES.times do
151
+ upload_status_response = get_upload_status(app_id, upload_token)
152
+ if upload_status_response.success? || upload_status_response.already_uploaded?
153
+ UI.success("✅ Uploaded the #{@binary_type}.")
154
+ break
155
+ elsif upload_status_response.in_progress?
156
+ sleep(POLLING_INTERVAL_SECONDS)
157
+ else
158
+ if !upload_status_response.message.nil?
159
+ UI.user_error!("#{ErrorMessage.upload_binary_error(@binary_type)}: #{upload_status_response.message}")
160
+ else
161
+ UI.user_error!(ErrorMessage.upload_binary_error(@binary_type))
162
+ end
163
+ end
164
+ end
165
+ unless upload_status_response.success?
166
+ UI.error("It took longer than expected to process your #{@binary_type}, please try again.")
167
+ return nil
168
+ end
169
+ end
170
+ upload_status_response.release_id
171
+ end
172
+
173
+ # Fetches the status of an uploaded binary
174
+ #
175
+ # args
176
+ # app_id - Firebase App ID
177
+ # upload_token - URL encoded upload token
178
+ #
179
+ # Returns the release ID on a successful release, otherwise returns nil.
180
+ #
181
+ # Throws a user_error if an invalid app_id is passed in
182
+ def get_upload_status(app_id, upload_token)
183
+ begin
184
+ response = connection.get(upload_status_url(app_id, upload_token)) do |request|
185
+ request.headers["Authorization"] = "Bearer " + @auth_token
186
+ end
187
+ rescue Faraday::ResourceNotFound
188
+ UI.user_error!("#{ErrorMessage::INVALID_APP_ID}: #{app_id}")
189
+ end
190
+ return UploadStatusResponse.new(response.body)
191
+ end
192
+
193
+ private
194
+
195
+ def v1_apps_url(app_id)
196
+ "/v1alpha/apps/#{app_id}"
197
+ end
198
+
199
+ def release_notes_create_url(app_id, release_id)
200
+ "#{v1_apps_url(app_id)}/releases/#{release_id}/notes"
201
+ end
202
+
203
+ def enable_access_url(app_id, release_id)
204
+ "#{v1_apps_url(app_id)}/releases/#{release_id}/enable_access"
205
+ end
206
+
207
+ def binary_upload_url(app_id)
208
+ "/app-binary-uploads?app_id=#{app_id}"
209
+ end
210
+
211
+ def upload_status_url(app_id, app_token)
212
+ "#{v1_apps_url(app_id)}/upload_status/#{app_token}"
213
+ end
214
+
215
+ def upload_token_format(app_id, project_number, binary_hash)
216
+ CGI.escape("projects/#{project_number}/apps/#{app_id}/releases/-/binaries/#{binary_hash}")
217
+ end
218
+
219
+ def connection
220
+ @connection ||= Faraday.new(url: BASE_URL) do |conn|
221
+ conn.response(:json, parser_options: { symbolize_names: true })
222
+ conn.response(:raise_error) # raise_error middleware will run before the json middleware
223
+ conn.adapter(Faraday.default_adapter)
224
+ end
225
+ end
226
+ end
227
+ end
228
+ end
@@ -0,0 +1,91 @@
1
+ require 'fastlane_core/ui/ui'
2
+ module Fastlane
3
+ UI = FastlaneCore::UI unless Fastlane.const_defined?("UI")
4
+ module Auth
5
+ module FirebaseAppDistributionAuthClient
6
+ TOKEN_CREDENTIAL_URI = "https://oauth2.googleapis.com/token"
7
+
8
+ # Returns the auth token for any of the auth methods (Firebase CLI token,
9
+ # Google service account, firebase-tools). To ensure that a specific
10
+ # auth method is used, unset all other auth variables/parameters to nil/empty
11
+ #
12
+ # args
13
+ # 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
16
+ #
17
+ # env variables
18
+ # GOOGLE_APPLICATION_CREDENTIALS - see google_service_path
19
+ # FIREBASE_TOKEN - see firebase_cli_token
20
+ #
21
+ # Crashes if given invalid or missing credentials
22
+ def fetch_auth_token(google_service_path, firebase_cli_token)
23
+ 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)
26
+ 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)
29
+ 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"])
32
+ 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"])
35
+ 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)
38
+ else
39
+ UI.user_error!(ErrorMessage::MISSING_CREDENTIALS)
40
+ end
41
+ UI.success("🔐 Authenticated successfully.")
42
+ token
43
+ end
44
+
45
+ private
46
+
47
+ 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
+
54
+ if File.exist?(config_path)
55
+ 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
61
+ end
62
+ end
63
+ end
64
+
65
+ def firebase_token(refresh_token)
66
+ client = Signet::OAuth2::Client.new(
67
+ token_credential_uri: TOKEN_CREDENTIAL_URI,
68
+ client_id: Fastlane::Actions::FirebaseAppDistributionLoginAction::CLIENT_ID,
69
+ client_secret: Fastlane::Actions::FirebaseAppDistributionLoginAction::CLIENT_SECRET,
70
+ refresh_token: refresh_token
71
+ )
72
+ client.fetch_access_token!
73
+ client.access_token
74
+ rescue Signet::AuthorizationError
75
+ UI.user_error!(ErrorMessage::REFRESH_TOKEN_ERROR)
76
+ end
77
+
78
+ def service_account(google_service_path)
79
+ service_account_credentials = Google::Auth::ServiceAccountCredentials.make_creds(
80
+ json_key_io: File.open(google_service_path),
81
+ scope: Fastlane::Actions::FirebaseAppDistributionLoginAction::SCOPE
82
+ )
83
+ service_account_credentials.fetch_access_token!["access_token"]
84
+ rescue Errno::ENOENT
85
+ 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}")
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,34 @@
1
+ module ErrorMessage
2
+ 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."
3
+ MISSING_APP_ID = "Missing app id. Please check that the app parameter is set and try again"
4
+ SERVICE_CREDENTIALS_NOT_FOUND = "Service credentials file does not exist. Please check the service credentials path and try again"
5
+ PARSE_SERVICE_CREDENTIALS_ERROR = "Failed to extract service account information from the service credentials file"
6
+ UPLOAD_RELEASE_NOTES_ERROR = "App Distribution halted because it had a problem uploading release notes"
7
+ UPLOAD_TESTERS_ERROR = "App Distribution halted because it had a problem adding testers/groups"
8
+ GET_RELEASE_TIMEOUT = "App Distribution failed to fetch release information"
9
+ 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
+ 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
+ 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"
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"
17
+ SERVICE_CREDENTIALS_ERROR = "App Distribution could not generate credentials from the service credentials file specified. Service Account Path"
18
+
19
+ def self.binary_not_found(binary_type)
20
+ "Could not find the #{binary_type}. Make sure you set the #{binary_type} path parameter to point to your #{binary_type}"
21
+ end
22
+
23
+ def self.parse_binary_metadata_error(binary_type)
24
+ "Failed to extract #{binary_type} metadata from the #{binary_type} path"
25
+ end
26
+
27
+ def self.upload_binary_error(binary_type)
28
+ "App Distribution halted because it had a problem uploading the #{binary_type}"
29
+ end
30
+
31
+ def self.binary_processing_error(binary_type)
32
+ "App Distribution failed to process the #{binary_type}"
33
+ end
34
+ end
@@ -1,60 +1,39 @@
1
1
  require 'fastlane_core/ui/ui'
2
-
2
+ require 'cfpropertylist'
3
3
  module Fastlane
4
4
  UI = FastlaneCore::UI unless Fastlane.const_defined?("UI")
5
-
6
5
  module Helper
7
6
  module FirebaseAppDistributionHelper
8
- def testers_flag(params)
9
- file_flag_if_supplied("--testers-file", "testers", params)
10
- end
11
-
12
- def groups_flag(params)
13
- file_flag_if_supplied("--groups-file", "groups", params)
14
- end
15
-
16
- def release_notes_flag(params)
17
- file_flag_if_supplied("--release-notes-file", "release_notes", params)
18
- end
19
-
20
- def file_flag_if_supplied(flag, param_name, params)
21
- file = params["#{param_name}_file".to_sym]
22
- file ||= file_for_contents(param_name.to_sym, params)
23
-
24
- if file
25
- return "#{flag} #{file}"
7
+ def get_value_from_value_or_file(value, path)
8
+ if (value.nil? || value.empty?) && !path.nil?
9
+ begin
10
+ return File.open(path).read
11
+ rescue Errno::ENOENT
12
+ UI.crash!("#{ErrorMessage::INVALID_PATH}: #{path}")
13
+ end
26
14
  end
15
+ value
27
16
  end
28
17
 
29
- def flag_value_if_supplied(flag, param_name, params)
30
- "#{flag} #{params[param_name]}" if params[param_name]
31
- end
32
-
33
- def flag_if_supplied(flag, param_name, params)
34
- flag if params[param_name]
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"
21
+ def string_to_array(string)
22
+ return nil if string.nil? || string.empty?
23
+ string_array = string.gsub(/\s+/, '').split(",")
24
+ return string_array
35
25
  end
36
26
 
37
- ##
38
- # always return a file for a given content
39
- def file_for_contents(parameter_name, params)
40
- if @tempfiles.nil?
41
- @tempfiles = []
42
- end
43
-
44
- contents = params[parameter_name]
45
- return nil if contents.nil?
46
-
47
- file = Tempfile.new(parameter_name.to_s)
48
- file.write(contents)
49
- file.close
50
- @tempfiles << file
51
-
52
- file.path
27
+ def parse_plist(path)
28
+ CFPropertyList.native_types(CFPropertyList::List.new(file: path).value)
53
29
  end
54
30
 
55
- def cleanup_tempfiles
56
- return if @tempfiles.nil?
57
- @tempfiles.each(&:unlink)
31
+ def get_ios_app_id_from_archive(path)
32
+ app_path = parse_plist("#{path}/Info.plist")["ApplicationProperties"]["ApplicationPath"]
33
+ UI.shell_error!("can't extract application path from Info.plist at #{path}") if app_path.empty?
34
+ identifier = parse_plist("#{path}/Products/#{app_path}/GoogleService-Info.plist")["GOOGLE_APP_ID"]
35
+ UI.shell_error!("can't extract GOOGLE_APP_ID") if identifier.empty?
36
+ return identifier
58
37
  end
59
38
  end
60
39
  end
@@ -0,0 +1,37 @@
1
+ class UploadStatusResponse
2
+ def initialize(response_json_hash)
3
+ @response_json_hash = response_json_hash
4
+ end
5
+
6
+ def status
7
+ @response_json_hash[:status]
8
+ end
9
+
10
+ def success?
11
+ status == 'SUCCESS'
12
+ end
13
+
14
+ def in_progress?
15
+ status == 'IN_PROGRESS'
16
+ end
17
+
18
+ def error?
19
+ status == 'ERROR'
20
+ end
21
+
22
+ def already_uploaded?
23
+ status == 'ALREADY_UPLOADED'
24
+ end
25
+
26
+ def release_hash
27
+ @response_json_hash[:release]
28
+ end
29
+
30
+ def release_id
31
+ release_hash ? release_hash[:id] : nil
32
+ end
33
+
34
+ def message
35
+ @response_json_hash[:message]
36
+ end
37
+ end
@@ -1,5 +1,5 @@
1
1
  module Fastlane
2
2
  module FirebaseAppDistribution
3
- VERSION = "0.1.4"
3
+ VERSION = "0.2.0"
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,14 +1,16 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fastlane-plugin-firebase_app_distribution
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.4
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stefan Natchev
8
- autorequire:
8
+ - Manny Jimenez
9
+ - Alonso Salas Infante
10
+ autorequire:
9
11
  bindir: bin
10
12
  cert_chain: []
11
- date: 2019-10-09 00:00:00.000000000 Z
13
+ date: 2020-09-02 00:00:00.000000000 Z
12
14
  dependencies:
13
15
  - !ruby/object:Gem::Dependency
14
16
  name: pry
@@ -136,9 +138,11 @@ dependencies:
136
138
  - - ">="
137
139
  - !ruby/object:Gem::Version
138
140
  version: 2.127.1
139
- description:
141
+ description:
140
142
  email:
141
143
  - snatchev@google.com
144
+ - mannyjimenez@google.com
145
+ - alonsosi@google.com
142
146
  executables: []
143
147
  extensions: []
144
148
  extra_rdoc_files: []
@@ -147,13 +151,18 @@ files:
147
151
  - README.md
148
152
  - lib/fastlane/plugin/firebase_app_distribution.rb
149
153
  - 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/client/firebase_app_distribution_api_client.rb
156
+ - lib/fastlane/plugin/firebase_app_distribution/helper/firebase_app_distribution_auth_client.rb
157
+ - lib/fastlane/plugin/firebase_app_distribution/helper/firebase_app_distribution_error_message.rb
150
158
  - lib/fastlane/plugin/firebase_app_distribution/helper/firebase_app_distribution_helper.rb
159
+ - lib/fastlane/plugin/firebase_app_distribution/helper/upload_status_response.rb
151
160
  - lib/fastlane/plugin/firebase_app_distribution/version.rb
152
- homepage: https://github.com/fastlane-community/fastlane-plugin-firebase_app_distribution
161
+ homepage: https://github.com/fastlane/fastlane-plugin-firebase_app_distribution
153
162
  licenses:
154
163
  - MIT
155
164
  metadata: {}
156
- post_install_message:
165
+ post_install_message:
157
166
  rdoc_options: []
158
167
  require_paths:
159
168
  - lib
@@ -168,8 +177,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
168
177
  - !ruby/object:Gem::Version
169
178
  version: '0'
170
179
  requirements: []
171
- rubygems_version: 3.0.6
172
- signing_key:
180
+ rubygems_version: 3.1.2
181
+ signing_key:
173
182
  specification_version: 4
174
183
  summary: Release your beta builds to Firebase App Distribution. https://firebase.google.com/docs/app-distribution
175
184
  test_files: []