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 +4 -4
- data/README.md +6 -17
- data/lib/fastlane/plugin/firebase_app_distribution/actions/firebase_app_distribution_action.rb +56 -35
- data/lib/fastlane/plugin/firebase_app_distribution/actions/firebase_app_distribution_login.rb +58 -0
- data/lib/fastlane/plugin/firebase_app_distribution/client/firebase_app_distribution_api_client.rb +213 -0
- data/lib/fastlane/plugin/firebase_app_distribution/helper/firebase_app_distribution_auth_client.rb +91 -0
- data/lib/fastlane/plugin/firebase_app_distribution/helper/firebase_app_distribution_error_message.rb +34 -0
- data/lib/fastlane/plugin/firebase_app_distribution/helper/firebase_app_distribution_helper.rb +25 -38
- data/lib/fastlane/plugin/firebase_app_distribution/helper/upload_status_response.rb +37 -0
- data/lib/fastlane/plugin/firebase_app_distribution/version.rb +1 -1
- metadata +18 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c591cb4a3b841f9fc26c94f3ff32b78b321ed904cc176e05a053e330543cd535
|
4
|
+
data.tar.gz: 0114a22cc34eec1970b5b280746b46b9d858407bbb5c1c7b3f3d54842e75b4d5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
8
|
-
|
9
|
-
```bash
|
10
|
-
fastlane add_plugin firebase_app_distribution
|
11
|
-
```
|
3
|
+
# ![Firebase App Distribution](fad-icon.png) Firebase App Distribution
|
12
4
|
|
13
|
-
|
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
|
-
|
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
|
-
|
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
|
|
data/lib/fastlane/plugin/firebase_app_distribution/actions/firebase_app_distribution_action.rb
CHANGED
@@ -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
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
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.
|
52
|
-
platform
|
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:
|
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
|
-
|
92
|
-
|
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
|
data/lib/fastlane/plugin/firebase_app_distribution/client/firebase_app_distribution_api_client.rb
ADDED
@@ -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
|
data/lib/fastlane/plugin/firebase_app_distribution/helper/firebase_app_distribution_auth_client.rb
ADDED
@@ -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
|
data/lib/fastlane/plugin/firebase_app_distribution/helper/firebase_app_distribution_error_message.rb
ADDED
@@ -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
|
data/lib/fastlane/plugin/firebase_app_distribution/helper/firebase_app_distribution_helper.rb
CHANGED
@@ -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
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
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
|
-
#
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
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
|
-
|
27
|
+
def parse_plist(path)
|
28
|
+
CFPropertyList.native_types(CFPropertyList::List.new(file: path).value)
|
45
29
|
end
|
46
30
|
|
47
|
-
def
|
48
|
-
|
49
|
-
|
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
|
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.
|
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:
|
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:
|
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:
|
178
|
+
version: 1.3.1
|
169
179
|
requirements: []
|
170
|
-
rubygems_version: 3.
|
180
|
+
rubygems_version: 3.1.2
|
171
181
|
signing_key:
|
172
182
|
specification_version: 4
|
173
|
-
summary: Release your beta builds to Firebase App
|
183
|
+
summary: Release your beta builds to Firebase App Distribution. https://firebase.google.com/docs/app-distribution
|
174
184
|
test_files: []
|