fastlane-plugin-firebase_app_distribution 0.1.1 → 0.2.0.pre.2

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: 3322484a56333009e014da42a52738c805177e1343056767b59d6bf92752f511
4
- data.tar.gz: d3544fef79f17bae724fa9ced433ab6d5f1b35d98548633c85c16cae9772a585
3
+ metadata.gz: 7387fdbb5db28a63ea91ef6e4feb290f4cdb71b93e8c7132595098f94f1ffc16
4
+ data.tar.gz: d24c551ec1c81f90ca38307aa75f7c3884982fdc58de2911f54b5971056a4a99
5
5
  SHA512:
6
- metadata.gz: 39465b098a4d080ace440a5d73363d676497bc544f6563d57fde89e72cd37b8a9111d44ca98815ba633744d41045a13654c7e46c2f8f64e1b60669b7ad3944d4
7
- data.tar.gz: 22079d114aa0558ecdb5e670c05eff74faa68b286b15a2ff5eafd5dabfc6837202b023ee93cfc73969c3961178ae404695c1ef031cb01885a878cae60a3c16ef
6
+ metadata.gz: fe83dbc6eaca609c5471732598f004775ff2139d9d24487bb5021d821a909dfce61f955166c3ed1e115186909c7b455a0578237923845a5015d5ac776c57fe5c
7
+ data.tar.gz: 440ac81fb3b05743c511d22f2096c099a636cfc6df06a8d4a8703f9dff8d727c4841f6218e4503cec22d765d2379dc5bbca4ae76bc2b98dcb79007d2b45533ce
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,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`.chomp
16
+ DEFAULT_FIREBASE_CLI_PATH = `which firebase`
13
17
  FIREBASECMD_ACTION = "appdistribution:distribute".freeze
14
18
 
19
+ extend Auth::FirebaseAppDistributionAuthClient
15
20
  extend Helper::FirebaseAppDistributionHelper
16
21
 
17
22
  def self.run(params)
18
23
  params.values # to validate all inputs before looking for the ipa/apk
24
+ auth_token = fetch_auth_token(params[:service_credentials_file], params[:firebase_cli_token])
25
+ fad_api_client = Client::FirebaseAppDistributionApiClient.new(auth_token, platform)
26
+ binary_path = params[:ipa_path] || params[:apk_path]
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
19
44
 
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
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")
36
54
  end
37
55
 
38
56
  def self.description
@@ -40,7 +58,7 @@ module Fastlane
40
58
  end
41
59
 
42
60
  def self.authors
43
- ["Stefan Natchev"]
61
+ ["Stefan Natchev", "Manny Jimenez Github: mannyjimenez0810, Alonso Salas Infante Github: alonsosalasinfante"]
44
62
  end
45
63
 
46
64
  # supports markdown.
@@ -48,9 +66,11 @@ module Fastlane
48
66
  "Release your beta builds with Firebase App Distribution"
49
67
  end
50
68
 
51
- def self.available_options
52
- platform = Actions.lane_context[Actions::SharedValues::PLATFORM_NAME]
69
+ def self.platform
70
+ @platform ||= Actions.lane_context[Actions::SharedValues::PLATFORM_NAME]
71
+ end
53
72
 
73
+ def self.available_options
54
74
  if platform == :ios || platform.nil?
55
75
  ipa_path_default = Dir["*.ipa"].sort_by { |x| File.mtime(x) }.last
56
76
  end
@@ -83,7 +103,7 @@ module Fastlane
83
103
  FastlaneCore::ConfigItem.new(key: :app,
84
104
  env_name: "FIREBASEAPPDISTRO_APP",
85
105
  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,
106
+ optional: true,
87
107
  type: String),
88
108
  FastlaneCore::ConfigItem.new(key: :firebase_cli_path,
89
109
  env_name: "FIREBASEAPPDISTRO_FIREBASE_CLI_PATH",
@@ -93,6 +113,7 @@ module Fastlane
93
113
  optional: false,
94
114
  type: String,
95
115
  verify_block: proc do |value|
116
+ value.chomp!
96
117
  if value.to_s == "" || !File.exist?(value)
97
118
  UI.user_error!("firebase_cli_path: missing path to firebase cli tool. Please install firebase in $PATH or specify path")
98
119
  end
@@ -132,6 +153,19 @@ module Fastlane
132
153
  env_name: "FIREBASEAPPDISTRO_RELEASE_NOTES_FILE",
133
154
  description: "Release notes file for this build",
134
155
  optional: true,
156
+ type: String),
157
+ FastlaneCore::ConfigItem.new(key: :firebase_cli_token,
158
+ description: "Auth token for firebase cli",
159
+ optional: true,
160
+ type: String),
161
+ FastlaneCore::ConfigItem.new(key: :debug,
162
+ description: "Print verbose debug output",
163
+ optional: true,
164
+ default_value: false,
165
+ is_string: false),
166
+ FastlaneCore::ConfigItem.new(key: :service_credentials_file,
167
+ description: "Path to Google service account json",
168
+ optional: true,
135
169
  type: String)
136
170
  ]
137
171
  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("Please open the following address in your browser:")
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
+
25
+ UI.message("Refresh Token: #{credentials.refresh_token}")
26
+ UI.message("")
27
+ UI.message("Set the refresh token as a FIREBASE_TOKEN environment variable")
28
+ rescue Signet::AuthorizationError
29
+ UI.error("The code you entered was invalid. Ensure that you have copied the code correctly.")
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,202 @@
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
+ if platform == :ios || platform.nil?
15
+ @binary_type = "IPA"
16
+ else
17
+ @binary_type = "APK"
18
+ end
19
+ end
20
+
21
+ # Enables tester access to the specified app release. Skips this
22
+ # step if no testers are passed in (emails and group_ids are nil/empty).
23
+ #
24
+ # args
25
+ # app_id - Firebase App ID
26
+ # release_id - App release ID, returned by upload_status endpoint
27
+ # emails - String array of app testers' email addresses
28
+ # group_ids - String array of Firebase tester group IDs
29
+ #
30
+ # Throws a user_error if app_id, emails, or group_ids are invalid
31
+ def enable_access(app_id, release_id, emails, group_ids)
32
+ if (emails.nil? || emails.empty?) && (group_ids.nil? || group_ids.empty?)
33
+ UI.message("No testers passed in. Skipping this step")
34
+ return
35
+ end
36
+ payload = { emails: emails, groupIds: group_ids }
37
+ begin
38
+ connection.post(enable_access_url(app_id, release_id), payload.to_json) do |request|
39
+ request.headers["Authorization"] = "Bearer " + @auth_token
40
+ end
41
+ rescue Faraday::ResourceNotFound
42
+ UI.user_error!("#{ErrorMessage::INVALID_APP_ID}: #{app_id}")
43
+ rescue Faraday::ClientError
44
+ UI.user_error!("#{ErrorMessage::INVALID_TESTERS} \nEmails: #{emails} \nGroups: #{group_ids}")
45
+ end
46
+ UI.success("Added testers/groups successfully.")
47
+ end
48
+
49
+ # Posts notes for the specified app release. Skips this
50
+ # step if no notes are passed in (release_notes is nil/empty).
51
+ #
52
+ # args
53
+ # app_id - Firebase App ID
54
+ # release_id - App release ID, returned by upload_status endpoint
55
+ # release_notes - String of notes for this release
56
+ #
57
+ # Throws a user_error if app_id or release_id are invalid
58
+ def post_notes(app_id, release_id, release_notes)
59
+ payload = { releaseNotes: { releaseNotes: release_notes } }
60
+ if release_notes.nil? || release_notes.empty?
61
+ UI.message("No release notes passed in. Skipping this step.")
62
+ return
63
+ end
64
+ begin
65
+ connection.post(release_notes_create_url(app_id, release_id), payload.to_json) do |request|
66
+ request.headers["Authorization"] = "Bearer " + @auth_token
67
+ end
68
+ rescue Faraday::ResourceNotFound
69
+ UI.user_error!("#{ErrorMessage::INVALID_APP_ID}: #{app_id}")
70
+ # rescue Faraday::ClientError
71
+ # UI.user_error!("#{ErrorMessage::INVALID_RELEASE_ID}: #{release_id}")
72
+ end
73
+ UI.success("Release notes have been posted.")
74
+ end
75
+
76
+ def get_upload_token(app_id, binary_path)
77
+ begin
78
+ binary_hash = Digest::SHA256.hexdigest(File.open(binary_path).read)
79
+ rescue Errno::ENOENT
80
+ UI.crash!("#{ErrorMessage.binary_not_found(@binary_type)}: #{binary_path}")
81
+ end
82
+
83
+ begin
84
+ response = connection.get(v1_apps_url(app_id)) do |request|
85
+ request.headers["Authorization"] = "Bearer " + @auth_token
86
+ end
87
+ rescue Faraday::ResourceNotFound
88
+ UI.crash!("#{ErrorMessage::INVALID_APP_ID}: #{app_id}")
89
+ end
90
+ contact_email = response.body[:contactEmail]
91
+ if contact_email.nil? || contact_email.strip.empty?
92
+ UI.crash!(ErrorMessage::GET_APP_NO_CONTACT_EMAIL_ERROR)
93
+ end
94
+ return upload_token_format(response.body[:appId], response.body[:projectNumber], binary_hash)
95
+ end
96
+
97
+ def upload_binary(app_id, binary_path, platform)
98
+ connection.post(binary_upload_url(app_id), File.open(binary_path).read) do |request|
99
+ request.headers["Authorization"] = "Bearer " + @auth_token
100
+ request.headers["X-APP-DISTRO-API-CLIENT-ID"] = "fastlane"
101
+ request.headers["X-APP-DISTRO-API-CLIENT-TYPE"] = platform
102
+ request.headers["X-APP-DISTRO-API-CLIENT-VERSION"] = Fastlane::FirebaseAppDistribution::VERSION
103
+ end
104
+ rescue Faraday::ResourceNotFound
105
+ UI.crash!("#{ErrorMessage::INVALID_APP_ID}: #{app_id}")
106
+ rescue Errno::ENOENT
107
+ UI.crash!("#{ErrorMessage.binary_not_found(@binary_type)}: #{binary_path}")
108
+ end
109
+
110
+ # Uploads the binary file if it has not already been uploaded
111
+ # Takes at least POLLING_INTERVAL_SECONDS between polling get_upload_status
112
+ #
113
+ # args
114
+ # app_id - Firebase App ID
115
+ # binary_path - Absolute path to your app's apk/ipa file
116
+ #
117
+ # Returns the release_id on a successful release, otherwise returns nil.
118
+ #
119
+ # Throws a UI error if the number of polling retries exceeds MAX_POLLING_RETRIES
120
+ # Crashes if not able to upload the binary
121
+ def upload(app_id, binary_path, platform)
122
+ upload_token = get_upload_token(app_id, binary_path)
123
+ upload_status_response = get_upload_status(app_id, upload_token)
124
+ if upload_status_response.success? || upload_status_response.already_uploaded?
125
+ UI.success("This #{@binary_type} has been uploaded before. Skipping upload step.")
126
+ else
127
+ UI.message("This #{@binary_type} has not been uploaded before")
128
+ UI.message("Uploading the #{@binary_type}.")
129
+ unless upload_status_response.in_progress?
130
+ upload_binary(app_id, binary_path, platform)
131
+ end
132
+ MAX_POLLING_RETRIES.times do
133
+ upload_status_response = get_upload_status(app_id, upload_token)
134
+ if upload_status_response.success? || upload_status_response.already_uploaded?
135
+ UI.success("Uploaded #{@binary_type} Successfully!")
136
+ break
137
+ elsif upload_status_response.in_progress?
138
+ sleep(POLLING_INTERVAL_SECONDS)
139
+ else
140
+ if !upload_status_response.message.nil?
141
+ UI.user_error!("#{ErrorMessage.upload_binary_error(@binary_type)}: #{upload_status_response.message}")
142
+ else
143
+ UI.user_error!(ErrorMessage.upload_binary_error(@binary_type))
144
+ end
145
+ end
146
+ end
147
+ unless upload_status_response.success?
148
+ UI.error("It took longer than expected to process your #{@binary_type}, please try again.")
149
+ return nil
150
+ end
151
+ end
152
+ upload_status_response.release_id
153
+ end
154
+
155
+ # Gets the upload status for the app release.
156
+ def get_upload_status(app_id, app_token)
157
+ begin
158
+ response = connection.get(upload_status_url(app_id, app_token)) do |request|
159
+ request.headers["Authorization"] = "Bearer " + @auth_token
160
+ end
161
+ rescue Faraday::ResourceNotFound
162
+ UI.crash!("#{ErrorMessage::INVALID_APP_ID}: #{app_id}")
163
+ end
164
+ return UploadStatusResponse.new(response.body)
165
+ end
166
+
167
+ private
168
+
169
+ def v1_apps_url(app_id)
170
+ "/v1alpha/apps/#{app_id}"
171
+ end
172
+
173
+ def release_notes_create_url(app_id, release_id)
174
+ "#{v1_apps_url(app_id)}/releases/#{release_id}/notes"
175
+ end
176
+
177
+ def enable_access_url(app_id, release_id)
178
+ "#{v1_apps_url(app_id)}/releases/#{release_id}/enable_access"
179
+ end
180
+
181
+ def binary_upload_url(app_id)
182
+ "/app-binary-uploads?app_id=#{app_id}"
183
+ end
184
+
185
+ def upload_status_url(app_id, app_token)
186
+ "#{v1_apps_url(app_id)}/upload_status/#{app_token}"
187
+ end
188
+
189
+ def upload_token_format(app_id, project_number, binary_hash)
190
+ CGI.escape("projects/#{project_number}/apps/#{app_id}/releases/-/binaries/#{binary_hash}")
191
+ end
192
+
193
+ def connection
194
+ @connection ||= Faraday.new(url: BASE_URL) do |conn|
195
+ conn.response(:json, parser_options: { symbolize_names: true })
196
+ conn.response(:raise_error) # raise_error middleware will run before the json middleware
197
+ conn.adapter(Faraday.default_adapter)
198
+ end
199
+ end
200
+ end
201
+ end
202
+ 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 it was passed in 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 = "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. Ensure 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 = "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.1"
3
+ VERSION = "0.2.0.pre.2"
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.1
4
+ version: 0.2.0.pre.2
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-06 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.4
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: []