fastlane-plugin-firebase_app_distribution 0.1.2 → 0.2.0.pre.3

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: 95ce0caafa7c11d0cb4796cbc744b6a1e1376c5edf3cdc38859960724bbb67a0
4
- data.tar.gz: fc59439517a29b7cab67717a7c359a5f5d79b51ff342eccf5f660e98cb7343b9
3
+ metadata.gz: c591cb4a3b841f9fc26c94f3ff32b78b321ed904cc176e05a053e330543cd535
4
+ data.tar.gz: 0114a22cc34eec1970b5b280746b46b9d858407bbb5c1c7b3f3d54842e75b4d5
5
5
  SHA512:
6
- metadata.gz: d992a8074bff8b4c5c08b1bd1dae8bf498be2dc7b9b8e0562e76c2eb38e2ace7784d858c782f3df2d344c587ca29a39c798b7c07244df680ae64e82b3ed05d13
7
- data.tar.gz: d65c13c342fda3ff0bae78f24388015a9220dfeb4d28e13c8d1c0e7c0f170461b6d3911e6b8e72b9cd0ecae6684596416c47f21f40aa03cef4e6e1ff6a858cf8
6
+ metadata.gz: 493da92f746defa50af47f9b2d920b65fa5fcbfbb085f0be74979fe7f26f684b78697be17c5e32eeac7fe1d0d78667ece5ef7198138358f6831870c382a5d753
7
+ data.tar.gz: 7afb5987aa4fd08032d4fa684b9c291d3c4dc762d2978b37bbe4e921ae5730a9a547819a49b61ee2d120617d961b13891b03f17f1ad48deb004eb125b658ad65
data/README.md CHANGED
@@ -1,26 +1,15 @@
1
- # firebase_app_distribution plugin
2
1
 
3
- [![fastlane Plugin Badge](https://rawcdn.githack.com/fastlane/fastlane/master/fastlane/assets/plugin-badge.svg)](https://rubygems.org/gems/fastlane-plugin-firebase_app_distribution)
4
-
5
- ## Getting Started
6
2
 
7
- This project is a [_fastlane_](https://github.com/fastlane/fastlane) plugin. To get started with `fastlane-plugin-firebase_app_distribution`, add it to your project by running:
8
-
9
- ```bash
10
- fastlane add_plugin firebase_app_distribution
11
- ```
3
+ # ![Firebase App Distribution](fad-icon.png) Firebase App Distribution
12
4
 
13
- ## About firebase_app_distribution
14
-
15
- Release your beta builds to Firebase App Distro
16
-
17
- **Note to author:** Add a more detailed description about this plugin here. If your plugin contains multiple actions, make sure to mention them here.
5
+ [![fastlane Plugin Badge](https://rawcdn.githack.com/fastlane/fastlane/master/fastlane/assets/plugin-badge.svg)](https://rubygems.org/gems/fastlane-plugin-firebase_app_distribution)
18
6
 
19
- ## Example
7
+ Firebase App Distribution makes distributing your apps to trusted testers painless. By getting your apps onto testers' devices quickly, you can get feedback early and often. To learn more about Firebase App Distribution, go [here](https://firebase.google.com/docs/app-distribution).
20
8
 
21
- Check out the [example `Fastfile`](fastlane/Fastfile) to see how to use this plugin. Try it by cloning the repo, running `fastlane install_plugins` and `bundle exec fastlane test`.
22
9
 
23
- **Note to author:** Please set up a sample project to make it easy for users to explore what your plugin does. Provide everything that is necessary to try out the plugin in this project (including a sample Xcode/Android project if necessary)
10
+ ## Getting Started
11
+ - [iOS](https://firebase.google.com/docs/app-distribution/ios/distribute-fastlane)
12
+ - [Android](https://firebase.google.com/docs/app-distribution/android/distribute-fastlane)
24
13
 
25
14
  ## Run tests for this plugin
26
15
 
@@ -1,38 +1,55 @@
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`.chomp
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
23
+ auth_token = fetch_auth_token(params[:service_credentials_file], params[:firebase_cli_token])
24
+ fad_api_client = Client::FirebaseAppDistributionApiClient.new(auth_token, platform)
25
+ binary_path = params[:ipa_path] || params[:apk_path]
26
+
27
+ if params[:app] # Set app_id if it is specified as a parameter
28
+ app_id = params[:app]
29
+ elsif platform == :ios
30
+ archive_path = Actions.lane_context[SharedValues::XCODEBUILD_ARCHIVE]
31
+ if archive_path
32
+ app_id = get_ios_app_id_from_archive(archive_path)
33
+ end
34
+ end
35
+
36
+ if app_id.nil?
37
+ UI.crash!(ErrorMessage::MISSING_APP_ID)
38
+ end
39
+ release_id = fad_api_client.upload(app_id, binary_path, platform.to_s)
40
+ if release_id.nil?
41
+ return
42
+ end
43
+
44
+ release_notes = get_value_from_value_or_file(params[:release_notes], params[:release_notes_file])
45
+ fad_api_client.post_notes(app_id, release_id, release_notes)
19
46
 
20
- cmd = [params[:firebase_cli_path], FIREBASECMD_ACTION]
21
- cmd << Shellwords.escape(params[:ipa_path] || params[:apk_path])
22
- cmd << "--app #{params[:app]}"
23
-
24
- cmd << groups_flag(params)
25
- cmd << testers_flag(params)
26
- cmd << release_notes_flag(params)
27
-
28
- Actions.sh_control_output(
29
- cmd.compact.join(" "),
30
- print_command: false,
31
- print_command_output: true
32
- )
33
- # make sure we do this, even in the case of an error.
34
- ensure
35
- cleanup_tempfiles
47
+ testers = get_value_from_value_or_file(params[:testers], params[:testers_file])
48
+ groups = get_value_from_value_or_file(params[:groups], params[:groups_file])
49
+ emails = string_to_array(testers)
50
+ group_ids = string_to_array(groups)
51
+ fad_api_client.enable_access(app_id, release_id, emails, group_ids)
52
+ UI.success("App Distribution upload finished successfully")
36
53
  end
37
54
 
38
55
  def self.description
@@ -40,7 +57,7 @@ module Fastlane
40
57
  end
41
58
 
42
59
  def self.authors
43
- ["Stefan Natchev"]
60
+ ["Stefan Natchev", "Manny Jimenez Github: mannyjimenez0810, Alonso Salas Infante Github: alonsosalasinfante"]
44
61
  end
45
62
 
46
63
  # supports markdown.
@@ -48,9 +65,11 @@ module Fastlane
48
65
  "Release your beta builds with Firebase App Distribution"
49
66
  end
50
67
 
51
- def self.available_options
52
- platform = Actions.lane_context[Actions::SharedValues::PLATFORM_NAME]
68
+ def self.platform
69
+ @platform ||= Actions.lane_context[Actions::SharedValues::PLATFORM_NAME]
70
+ end
53
71
 
72
+ def self.available_options
54
73
  if platform == :ios || platform.nil?
55
74
  ipa_path_default = Dir["*.ipa"].sort_by { |x| File.mtime(x) }.last
56
75
  end
@@ -83,24 +102,13 @@ module Fastlane
83
102
  FastlaneCore::ConfigItem.new(key: :app,
84
103
  env_name: "FIREBASEAPPDISTRO_APP",
85
104
  description: "Your app's Firebase App ID. You can find the App ID in the Firebase console, on the General Settings page",
86
- optional: false,
105
+ optional: true,
87
106
  type: String),
88
107
  FastlaneCore::ConfigItem.new(key: :firebase_cli_path,
89
108
  env_name: "FIREBASEAPPDISTRO_FIREBASE_CLI_PATH",
90
109
  description: "The absolute path of the firebase cli command",
91
- default_value: DEFAULT_FIREBASE_CLI_PATH,
92
- default_value_dynamic: true,
93
- optional: false,
94
- type: String,
95
- verify_block: proc do |value|
96
- if value.to_s == "" || !File.exist?(value)
97
- UI.user_error!("firebase_cli_path: missing path to firebase cli tool. Please install firebase in $PATH or specify path")
98
- end
99
-
100
- unless is_firebasecmd_supported?(value)
101
- 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")
102
- end
103
- end),
110
+ optional: true,
111
+ type: String),
104
112
  FastlaneCore::ConfigItem.new(key: :groups,
105
113
  env_name: "FIREBASEAPPDISTRO_GROUPS",
106
114
  description: "The groups used for distribution, separated by commas",
@@ -132,6 +140,19 @@ module Fastlane
132
140
  env_name: "FIREBASEAPPDISTRO_RELEASE_NOTES_FILE",
133
141
  description: "Release notes file for this build",
134
142
  optional: true,
143
+ type: String),
144
+ FastlaneCore::ConfigItem.new(key: :firebase_cli_token,
145
+ description: "Auth token for firebase cli",
146
+ optional: true,
147
+ type: String),
148
+ FastlaneCore::ConfigItem.new(key: :debug,
149
+ description: "Print verbose debug output",
150
+ optional: true,
151
+ default_value: false,
152
+ is_string: false),
153
+ FastlaneCore::ConfigItem.new(key: :service_credentials_file,
154
+ description: "Path to Google service account json",
155
+ optional: true,
135
156
  type: String)
136
157
  ]
137
158
  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,213 @@
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.message("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 successfully.")
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.message("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
+ def upload_binary(app_id, binary_path, platform)
109
+ connection.post(binary_upload_url(app_id), File.open(binary_path).read) do |request|
110
+ request.headers["Authorization"] = "Bearer " + @auth_token
111
+ request.headers["X-APP-DISTRO-API-CLIENT-ID"] = "fastlane"
112
+ request.headers["X-APP-DISTRO-API-CLIENT-TYPE"] = platform
113
+ request.headers["X-APP-DISTRO-API-CLIENT-VERSION"] = Fastlane::FirebaseAppDistribution::VERSION
114
+ end
115
+ rescue Faraday::ResourceNotFound
116
+ UI.crash!("#{ErrorMessage::INVALID_APP_ID}: #{app_id}")
117
+ rescue Errno::ENOENT
118
+ UI.crash!("#{ErrorMessage.binary_not_found(@binary_type)}: #{binary_path}")
119
+ end
120
+
121
+ # Uploads the binary file if it has not already been uploaded
122
+ # Takes at least POLLING_INTERVAL_SECONDS between polling get_upload_status
123
+ #
124
+ # args
125
+ # app_id - Firebase App ID
126
+ # binary_path - Absolute path to your app's apk/ipa file
127
+ #
128
+ # Returns the release_id on a successful release, otherwise returns nil.
129
+ #
130
+ # Throws a UI error if the number of polling retries exceeds MAX_POLLING_RETRIES
131
+ # Crashes if not able to upload the binary
132
+ def upload(app_id, binary_path, platform)
133
+ upload_token = get_upload_token(app_id, binary_path)
134
+ upload_status_response = get_upload_status(app_id, upload_token)
135
+ if upload_status_response.success? || upload_status_response.already_uploaded?
136
+ UI.success("This #{@binary_type} has been uploaded before. Skipping upload step.")
137
+ else
138
+ UI.message("This #{@binary_type} has not been uploaded before")
139
+ UI.message("Uploading the #{@binary_type}.")
140
+ unless upload_status_response.in_progress?
141
+ upload_binary(app_id, binary_path, platform)
142
+ end
143
+ MAX_POLLING_RETRIES.times do
144
+ upload_status_response = get_upload_status(app_id, upload_token)
145
+ if upload_status_response.success? || upload_status_response.already_uploaded?
146
+ UI.success("Uploaded #{@binary_type} successfully!")
147
+ break
148
+ elsif upload_status_response.in_progress?
149
+ sleep(POLLING_INTERVAL_SECONDS)
150
+ else
151
+ if !upload_status_response.message.nil?
152
+ UI.user_error!("#{ErrorMessage.upload_binary_error(@binary_type)}: #{upload_status_response.message}")
153
+ else
154
+ UI.user_error!(ErrorMessage.upload_binary_error(@binary_type))
155
+ end
156
+ end
157
+ end
158
+ unless upload_status_response.success?
159
+ UI.error("It took longer than expected to process your #{@binary_type}, please try again.")
160
+ return nil
161
+ end
162
+ end
163
+ upload_status_response.release_id
164
+ end
165
+
166
+ # Gets the upload status for the app release.
167
+ def get_upload_status(app_id, app_token)
168
+ begin
169
+ response = connection.get(upload_status_url(app_id, app_token)) do |request|
170
+ request.headers["Authorization"] = "Bearer " + @auth_token
171
+ end
172
+ rescue Faraday::ResourceNotFound
173
+ UI.crash!("#{ErrorMessage::INVALID_APP_ID}: #{app_id}")
174
+ end
175
+ return UploadStatusResponse.new(response.body)
176
+ end
177
+
178
+ private
179
+
180
+ def v1_apps_url(app_id)
181
+ "/v1alpha/apps/#{app_id}"
182
+ end
183
+
184
+ def release_notes_create_url(app_id, release_id)
185
+ "#{v1_apps_url(app_id)}/releases/#{release_id}/notes"
186
+ end
187
+
188
+ def enable_access_url(app_id, release_id)
189
+ "#{v1_apps_url(app_id)}/releases/#{release_id}/enable_access"
190
+ end
191
+
192
+ def binary_upload_url(app_id)
193
+ "/app-binary-uploads?app_id=#{app_id}"
194
+ end
195
+
196
+ def upload_status_url(app_id, app_token)
197
+ "#{v1_apps_url(app_id)}/upload_status/#{app_token}"
198
+ end
199
+
200
+ def upload_token_format(app_id, project_number, binary_hash)
201
+ CGI.escape("projects/#{project_number}/apps/#{app_id}/releases/-/binaries/#{binary_hash}")
202
+ end
203
+
204
+ def connection
205
+ @connection ||= Faraday.new(url: BASE_URL) do |conn|
206
+ conn.response(:json, parser_options: { symbolize_names: true })
207
+ conn.response(:raise_error) # raise_error middleware will run before the json middleware
208
+ conn.adapter(Faraday.default_adapter)
209
+ end
210
+ end
211
+ end
212
+ end
213
+ 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("Successfully authenticated!")
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,52 +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
- ##
30
- # always return a file for a given content
31
- def file_for_contents(parameter_name, params)
32
- if @tempfiles.nil?
33
- @tempfiles = []
34
- end
35
-
36
- contents = params[parameter_name]
37
- return nil if contents.nil?
38
-
39
- file = Tempfile.new(parameter_name.to_s)
40
- file.write(contents)
41
- file.close
42
- @tempfiles << file
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
25
+ end
43
26
 
44
- file.path
27
+ def parse_plist(path)
28
+ CFPropertyList.native_types(CFPropertyList::List.new(file: path).value)
45
29
  end
46
30
 
47
- def cleanup_tempfiles
48
- return if @tempfiles.nil?
49
- @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
50
37
  end
51
38
  end
52
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.2"
3
+ VERSION = "0.2.0.pre.3"
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.2
4
+ version: 0.2.0.pre.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stefan Natchev
8
+ - Manny Jimenez
9
+ - Alonso Salas Infante
8
10
  autorequire:
9
11
  bindir: bin
10
12
  cert_chain: []
11
- date: 2019-08-28 00:00:00.000000000 Z
13
+ date: 2020-08-18 00:00:00.000000000 Z
12
14
  dependencies:
13
15
  - !ruby/object:Gem::Dependency
14
16
  name: pry
@@ -137,7 +139,10 @@ dependencies:
137
139
  - !ruby/object:Gem::Version
138
140
  version: 2.127.1
139
141
  description:
140
- email: snatchev@google.com
142
+ email:
143
+ - snatchev@google.com
144
+ - mannyjimenez@google.com
145
+ - alonsosi@google.com
141
146
  executables: []
142
147
  extensions: []
143
148
  extra_rdoc_files: []
@@ -146,9 +151,14 @@ files:
146
151
  - README.md
147
152
  - lib/fastlane/plugin/firebase_app_distribution.rb
148
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
149
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
150
160
  - lib/fastlane/plugin/firebase_app_distribution/version.rb
151
- homepage:
161
+ homepage: https://github.com/fastlane/fastlane-plugin-firebase_app_distribution
152
162
  licenses:
153
163
  - MIT
154
164
  metadata: {}
@@ -163,12 +173,12 @@ required_ruby_version: !ruby/object:Gem::Requirement
163
173
  version: '0'
164
174
  required_rubygems_version: !ruby/object:Gem::Requirement
165
175
  requirements:
166
- - - ">="
176
+ - - ">"
167
177
  - !ruby/object:Gem::Version
168
- version: '0'
178
+ version: 1.3.1
169
179
  requirements: []
170
- rubygems_version: 3.0.1
180
+ rubygems_version: 3.1.2
171
181
  signing_key:
172
182
  specification_version: 4
173
- summary: Release your beta builds to Firebase App Distro
183
+ summary: Release your beta builds to Firebase App Distribution. https://firebase.google.com/docs/app-distribution
174
184
  test_files: []