fastlane-plugin-firebase_app_distribution 0.1.3 → 0.2.0.pre.4

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 839bb3358367db7178eed367005e3ede2a7b5073f25924ea71c96120afdfcbc2
4
- data.tar.gz: 4f0316d47fe8c3d0027cb24cc6389448c6e2372dd8413d83c4d6d0f49e7ff3a0
3
+ metadata.gz: b38b7e0efb3d03f71b9e4476b15304f9ffcddb75cd936acad55f16886222e458
4
+ data.tar.gz: '00228e7a41e3d55c4f0271ebbd5bd0aff4a699ec192b8e5e246a7dd271b9b5f3'
5
5
  SHA512:
6
- metadata.gz: 38eb5236916ab973cda79be4442dfcfe62faacf3915cf8875131bf4ae703085a30d65e17c3c765e6c53b6adfc66db14e2075d4538549dd68e74ed9342c1830e3
7
- data.tar.gz: 5382e2ddbe0081c20435d009826be4a83256aa65c93e48c34b2047f0bd38d074f44360b8939a45daef75546a06a3003259d6f73453ad8ff7f694af2ee6b55290
6
+ metadata.gz: 64478454789d88e5abdb0b8084e53b6e0d4320315073e59a3a5d789f2a7dde8adb34d79ca102148292977d763148645ad7b15ea2d8caeb21369a8932b9fd4f1e
7
+ data.tar.gz: ec77eb9cd77d44ebd4a3f3c6c89a5b3c37f3fc96a2c7bf8fa4ed227cbbca14b5c979583a75d555f2eba25912989d76672e9792eb0fe48cd18b63c56c7fc4b555
@@ -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 = [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.3"
3
+ VERSION = "0.2.0.pre.4"
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.3
4
+ version: 0.2.0.pre.4
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-08 00:00:00.000000000 Z
13
+ date: 2020-09-01 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
@@ -164,12 +173,12 @@ required_ruby_version: !ruby/object:Gem::Requirement
164
173
  version: '0'
165
174
  required_rubygems_version: !ruby/object:Gem::Requirement
166
175
  requirements:
167
- - - ">="
176
+ - - ">"
168
177
  - !ruby/object:Gem::Version
169
- version: '0'
178
+ version: 1.3.1
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: []