fastlane-plugin-match_keystore 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 02c4f28a1cdddaa21db5cc775e6e13b18317043120db76d70ef935d7947ad4e3
4
+ data.tar.gz: 81e444d74c3ffdc4ae3abb799ab449d2d3be0f66070d72826eec33e9223795ad
5
+ SHA512:
6
+ metadata.gz: 1dd9655b85a8744dc702d0208e9644cc6ecac754dc56759a46bd17c851c23b22e0f95d439d74e6d4f49b2e3b4229d39bbec89caf90329dd1014b9aefb7ae469e
7
+ data.tar.gz: 7ba84196c4c15b18ab5e4868fc8f85191bb0828973c35c64c431ae613fa66776cc543fb091ed78ec9795bb053b09461c335da18dd37749b8cec7cb193e5591a8
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2020 Christopher NEY <christopher.ney@gmail.com>
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,52 @@
1
+ # match_keystore plugin
2
+
3
+ [![fastlane Plugin Badge](https://rawcdn.githack.com/fastlane/fastlane/master/fastlane/assets/plugin-badge.svg)](https://rubygems.org/gems/fastlane-plugin-match_keystore)
4
+
5
+ ## Getting Started
6
+
7
+ This project is a [_fastlane_](https://github.com/fastlane/fastlane) plugin. To get started with `fastlane-plugin-match_keystore`, add it to your project by running:
8
+
9
+ ```bash
10
+ fastlane add_plugin match_keystore
11
+ ```
12
+
13
+ ## About match_keystore
14
+
15
+ Easily sync your Android keystores across your team
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.
18
+
19
+ ## Example
20
+
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
+
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)
24
+
25
+ ## Run tests for this plugin
26
+
27
+ To run both the tests, and code style validation, run
28
+
29
+ ```
30
+ rake
31
+ ```
32
+
33
+ To automatically fix many of the styling issues, use
34
+ ```
35
+ rubocop -a
36
+ ```
37
+
38
+ ## Issues and Feedback
39
+
40
+ For any other issues and feedback about this plugin, please submit it to this repository.
41
+
42
+ ## Troubleshooting
43
+
44
+ If you have trouble using plugins, check out the [Plugins Troubleshooting](https://docs.fastlane.tools/plugins/plugins-troubleshooting/) guide.
45
+
46
+ ## Using _fastlane_ Plugins
47
+
48
+ For more information about how the `fastlane` plugin system works, check out the [Plugins documentation](https://docs.fastlane.tools/plugins/create-plugin/).
49
+
50
+ ## About _fastlane_
51
+
52
+ _fastlane_ is the easiest way to automate beta deployments and releases for your iOS and Android apps. To learn more, check out [fastlane.tools](https://fastlane.tools).
@@ -0,0 +1,16 @@
1
+ require 'fastlane/plugin/match_keystore/version'
2
+
3
+ module Fastlane
4
+ module MatchKeystore
5
+ # Return all .rb files inside the "actions" and "helper" directory
6
+ def self.all_classes
7
+ Dir[File.expand_path('**/{actions,helper}/*.rb', File.dirname(__FILE__))]
8
+ end
9
+ end
10
+ end
11
+
12
+ # By default we want to import all available actions and helpers
13
+ # A plugin can contain any number of actions and plugins
14
+ Fastlane::MatchKeystore.all_classes.each do |current|
15
+ require current
16
+ end
@@ -0,0 +1,293 @@
1
+ require 'fastlane/action'
2
+ require 'fileutils'
3
+ require_relative '../helper/match_keystore_helper'
4
+
5
+ module Fastlane
6
+ module Actions
7
+ module SharedValues
8
+ MATCH_KEYSTORE_PATH = :MATCH_KEYSTORE_PATH
9
+ MATCH_KEYSTORE_PASSWORD = :MATCH_KEYSTORE_PASSWORD
10
+ MATCH_KEYSTORE_ALIAS_NAME = :MATCH_KEYSTORE_ALIAS_NAME
11
+ MATCH_KEYSTORE_ALIAS_PASSWORD = :MATCH_KEYSTORE_ALIAS_PASSWORD
12
+ end
13
+
14
+ class MatchKeystoreAction < Action
15
+
16
+ def self.load_properties(properties_filename)
17
+ properties = {}
18
+ File.open(properties_filename, 'r') do |properties_file|
19
+ properties_file.read.each_line do |line|
20
+ line.strip!
21
+ if (line[0] != ?# and line[0] != ?=)
22
+ i = line.index('=')
23
+ if (i)
24
+ properties[line[0..i - 1].strip] = line[i + 1..-1].strip
25
+ else
26
+ properties[line] = ''
27
+ end
28
+ end
29
+ end
30
+ end
31
+ properties
32
+ end
33
+
34
+ def self.get_android_home
35
+ puts `rm -f android_home.txt`
36
+ puts `echo $ANDROID_HOME > android_home.txt`
37
+ data = File.read("android_home.txt")
38
+ android_home = data.strip
39
+ puts `rm -f android_home.txt`
40
+ android_home
41
+ end
42
+
43
+ def self.get_build_tools
44
+ android_home = self.get_android_home()
45
+ build_tools_root = android_home + '/build-tools'
46
+
47
+ sub_dirs = Dir.glob(File.join(build_tools_root, '*', ''))
48
+ build_tools_last_version = ''
49
+ for sub_dir in sub_dirs
50
+ build_tools_last_version = sub_dir
51
+ end
52
+
53
+ build_tools_last_version
54
+ end
55
+
56
+ def self.gen_key(key_path, password)
57
+ puts `rm -f #{key_path}`
58
+ puts `echo "#{password}" | openssl dgst -sha512 | cut -c1-128 > #{key_path}`
59
+ end
60
+
61
+ def self.encrypt_file(clear_file, encrypt_file, key_path)
62
+ puts `rm -f #{encrypt_file}`
63
+ puts `openssl enc -aes-256-cbc -salt -in #{clear_file} -out #{encrypt_file} -pass file:#{key_path}`
64
+ end
65
+
66
+ def self.decrypt_file(encrypt_file, clear_file, key_path)
67
+ puts `rm -f #{clear_file}`
68
+ puts `openssl enc -d -aes-256-cbc -in #{encrypt_file} -out #{clear_file} -pass file:#{key_path}`
69
+ end
70
+
71
+ def self.sign_apk(apk_path, keystore_path, key_password, alias_name, alias_password, zip_align)
72
+
73
+ build_tools_path = self.get_build_tools()
74
+
75
+ # https://developer.android.com/studio/command-line/zipalign
76
+ if zip_align == true
77
+ apk_path_aligned = apk_path.gsub(".apk", "-aligned.apk")
78
+ puts `rm -f #{apk_path_aligned}`
79
+ puts `#{build_tools_path}zipalign -v 4 #{apk_path} #{apk_path_aligned}`
80
+ else
81
+ apk_path_aligned = apk_path
82
+ end
83
+
84
+ # https://developer.android.com/studio/command-line/apksigner
85
+ apk_path_signed = apk_path.gsub(".apk", "-signed.apk")
86
+ puts `rm -f #{apk_path_signed}`
87
+ puts `#{build_tools_path}apksigner sign --ks #{keystore_path} --ks-key-alias #{alias_name} --ks-pass pass:#{alias_password} --key-pass pass:#{key_password} --v1-signing-enabled true --v2-signing-enabled true --out #{apk_path_signed} #{apk_path_aligned}`
88
+
89
+ puts `#{build_tools_path}apksigner verify #{apk_path_signed}`
90
+ puts `rm -f #{apk_path_aligned}`
91
+
92
+ apk_path_signed
93
+ end
94
+
95
+ def self.run(params)
96
+
97
+ git_url = params[:git_url]
98
+ package_name = params[:package_name]
99
+ apk_path = params[:apk_path]
100
+ override_keystore = params[:override_keystore]
101
+
102
+ keystore_name = 'keystore.jks'
103
+ properties_name = 'keystore.properties'
104
+ properties_encrypt_name = 'keystore.properties.enc'
105
+
106
+ # Check Android Home env:
107
+ android_home = self.get_android_home()
108
+ UI.message("Android SDK: #{android_home}")
109
+ if android_home.to_s.strip.empty?
110
+ raise "The environment variable ANDROID_HOME is not defined, or Android SDK is not installed!"
111
+ end
112
+
113
+ dir_name = ENV['HOME'] + '/.match_keystore'
114
+ unless File.directory?(dir_name)
115
+ UI.message("Creating '.match_keystore' working directory...")
116
+ FileUtils.mkdir_p(dir_name)
117
+ end
118
+
119
+ key_path = dir_name + '/key.hex'
120
+ if !File.file?(key_path)
121
+ security_password = other_action.prompt(text: "Security password: ")
122
+ self.gen_key(key_path, security_password)
123
+ end
124
+
125
+ repo_dir = dir_name + '/repo'
126
+ unless File.directory?(repo_dir)
127
+ UI.message("Creating repository directory...")
128
+ FileUtils.mkdir_p(repo_dir)
129
+ end
130
+
131
+ gitDir = repo_dir + '/.git'
132
+ unless File.directory?(gitDir)
133
+ UI.message("Cloning remote Keystores repository...")
134
+ puts `git clone #{git_url} #{repo_dir}`
135
+ end
136
+
137
+ keystoreAppDir = repo_dir + '/' + package_name
138
+ unless File.directory?(keystoreAppDir)
139
+ UI.message("Creating '#{package_name}' keystore directory...")
140
+ FileUtils.mkdir_p(keystoreAppDir)
141
+ end
142
+
143
+ keystore_path = keystoreAppDir + '/' + keystore_name
144
+ properties_path = keystoreAppDir + '/' + properties_name
145
+ properties_encrypt_path = keystoreAppDir + '/' + properties_encrypt_name
146
+
147
+ # Create keystore with command
148
+ if !File.file?(keystore_path) || override_keystore
149
+
150
+ if File.file?(keystore_path)
151
+ FileUtils.remove_dir(keystore_path)
152
+ end
153
+
154
+ key_password = other_action.prompt(text: "Keystore Password: ")
155
+ alias_name = other_action.prompt(text: "Keystore Alias name: ")
156
+ alias_password = other_action.prompt(text: "Keystore Alias password: ")
157
+
158
+ full_name = other_action.prompt(text: "Certificate First and Last Name: ")
159
+ org_unit = other_action.prompt(text: "Certificate Organisation Unit: ")
160
+ org = other_action.prompt(text: "Certificate Organisation: ")
161
+ city_locality = other_action.prompt(text: "Certificate City or Locality: ")
162
+ state_province = other_action.prompt(text: "Certificate State or Province: ")
163
+ country = other_action.prompt(text: "Certificate Country Code (XX): ")
164
+
165
+ # https://developer.android.com/studio/publish/app-signing
166
+ UI.message("Generating Android Keystore...")
167
+ keytool_parts = [
168
+ "keytool -genkey -v",
169
+ "-keystore #{keystore_path}",
170
+ "-alias #{alias_name}",
171
+ "-keyalg RSA -keysize 2048 -validity 10000",
172
+ "-storepass #{alias_password} ",
173
+ "-keypass #{key_password}",
174
+ "-dname \"CN=#{full_name}, OU=#{org_unit}, O=#{org}, L=#{city_locality}, S=#{state_province}, C=#{country}\"",
175
+ ]
176
+ sh keytool_parts.join(" ")
177
+
178
+ UI.message("Generating Keystore properties...")
179
+
180
+ if File.file?(properties_path)
181
+ FileUtils.remove_dir(properties_path)
182
+ end
183
+
184
+ store_file = git_url + '/' + package_name + '/' + keystore_name
185
+
186
+ out_file = File.new(properties_path, "w")
187
+ out_file.puts("keyFile=#{store_file}")
188
+ out_file.puts("keyPassword=#{key_password}")
189
+ out_file.puts("aliasName=#{alias_name}")
190
+ out_file.puts("aliasPassword=#{alias_password}")
191
+ out_file.close
192
+
193
+ self.encrypt_file(properties_path, properties_encrypt_path, key_path)
194
+ File.delete(properties_path)
195
+
196
+ UI.message("Upload new Keystore to remote repository...")
197
+ puts `cd #{repo_dir} && git add .`
198
+ puts `cd #{repo_dir} && git commit -m "[ADD] Keystore for app '#{package_name}'."`
199
+ puts `cd #{repo_dir} && git push`
200
+
201
+ else
202
+ UI.message "Keystore file already exists, continue..."
203
+
204
+ self.decrypt_file(properties_encrypt_path, properties_path, key_path)
205
+
206
+ properties = self.load_properties(properties_path)
207
+ key_password = properties['keyPassword']
208
+ alias_name = properties['aliasName']
209
+ alias_password = properties['aliasPassword']
210
+
211
+ File.delete(properties_path)
212
+
213
+ end
214
+
215
+ UI.message("Preparing Keystore data context...")
216
+ Actions.lane_context[SharedValues::MATCH_KEYSTORE_PATH] = keystore_path
217
+ Actions.lane_context[SharedValues::MATCH_KEYSTORE_PASSWORD] = key_password
218
+ Actions.lane_context[SharedValues::MATCH_KEYSTORE_ALIAS_NAME] = alias_name
219
+ Actions.lane_context[SharedValues::MATCH_KEYSTORE_ALIAS_PASSWORD] = alias_password
220
+
221
+ if File.file?(keystore_path)
222
+ UI.message("Signing the APK...")
223
+ self.sign_apk(
224
+ apk_path,
225
+ keystore_path,
226
+ key_password,
227
+ alias_name,
228
+ alias_password,
229
+ true
230
+ )
231
+ end
232
+
233
+ end
234
+
235
+ def self.description
236
+ "Easily sync your Android keystores across your team"
237
+ end
238
+
239
+ def self.authors
240
+ ["Christopher NEY"]
241
+ end
242
+
243
+ def self.return_value
244
+ "Prepare Keystore local path, alias name, and passwords for the specified App."
245
+ end
246
+
247
+ def self.output
248
+ [
249
+ ['MATCH_KEYSTORE_PATH', 'File path of the Keystore fot the App.'],
250
+ ['MATCH_KEYSTORE_PASSWORD', 'Keystore password.'],
251
+ ['MATCH_KEYSTORE_ALIAS_NAME', 'Keystore Alias Name.'],
252
+ ['MATCH_KEYSTORE_ALIAS_PASSWORD', 'Keystore Alias Password.']
253
+ ]
254
+ end
255
+
256
+ def self.details
257
+ # Optional:
258
+ "This way, your entire team can use the same account and have one code signing identity without any manual work or confusion."
259
+ end
260
+
261
+ def self.available_options
262
+ [
263
+ FastlaneCore::ConfigItem.new(key: :git_url,
264
+ env_name: "MATCH_KEYSTORE_GIT_URL",
265
+ description: "The URL of the Git repository (Github, BitBucket...)",
266
+ optional: false,
267
+ type: String),
268
+ FastlaneCore::ConfigItem.new(key: :package_name,
269
+ env_name: "MATCH_KEYSTORE_PACKAGE_NAME",
270
+ description: "The package name of the App",
271
+ optional: false,
272
+ type: String),
273
+ FastlaneCore::ConfigItem.new(key: :apk_path,
274
+ env_name: "MATCH_KEYSTORE_APK_PATH",
275
+ description: "Path of the APK file ot sign with Keystore",
276
+ optional: false,
277
+ type: String),
278
+ FastlaneCore::ConfigItem.new(key: :override_keystore,
279
+ env_name: "MATCH_KEYSTORE_OVERRIDE",
280
+ description: "Override an existing Keystore (false by default)",
281
+ optional: true,
282
+ type: Boolean)
283
+ ]
284
+ end
285
+
286
+ def self.is_supported?(platform)
287
+ # Adjust this if your plugin only works for a particular platform (iOS vs. Android, for example)
288
+ # See: https://docs.fastlane.tools/advanced/#control-configuration-by-lane-and-by-platform
289
+ [:android].include?(platform)
290
+ end
291
+ end
292
+ end
293
+ end
@@ -0,0 +1,16 @@
1
+ require 'fastlane_core/ui/ui'
2
+
3
+ module Fastlane
4
+ UI = FastlaneCore::UI unless Fastlane.const_defined?("UI")
5
+
6
+ module Helper
7
+ class MatchKeystoreHelper
8
+ # class methods that you define here become available in your action
9
+ # as `Helper::MatchKeystoreHelper.your_method`
10
+ #
11
+ def self.show_message
12
+ UI.message("Hello from the match_keystore plugin helper!")
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,5 @@
1
+ module Fastlane
2
+ module MatchKeystore
3
+ VERSION = "0.1.0"
4
+ end
5
+ end
metadata ADDED
@@ -0,0 +1,174 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fastlane-plugin-match_keystore
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Christopher NEY
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2020-03-31 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: pry
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: bundler
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec_junit_formatter
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rake
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rubocop
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - '='
88
+ - !ruby/object:Gem::Version
89
+ version: 0.49.1
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - '='
95
+ - !ruby/object:Gem::Version
96
+ version: 0.49.1
97
+ - !ruby/object:Gem::Dependency
98
+ name: rubocop-require_tools
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: simplecov
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: fastlane
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: 2.144.0
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: 2.144.0
139
+ description:
140
+ email: christopher.ney@gmail.com
141
+ executables: []
142
+ extensions: []
143
+ extra_rdoc_files: []
144
+ files:
145
+ - LICENSE
146
+ - README.md
147
+ - lib/fastlane/plugin/match_keystore.rb
148
+ - lib/fastlane/plugin/match_keystore/actions/match_keystore_action.rb
149
+ - lib/fastlane/plugin/match_keystore/helper/match_keystore_helper.rb
150
+ - lib/fastlane/plugin/match_keystore/version.rb
151
+ homepage: https://github.com/christopherney/fastlane-plugin-match_keystore
152
+ licenses:
153
+ - MIT
154
+ metadata: {}
155
+ post_install_message:
156
+ rdoc_options: []
157
+ require_paths:
158
+ - lib
159
+ required_ruby_version: !ruby/object:Gem::Requirement
160
+ requirements:
161
+ - - ">="
162
+ - !ruby/object:Gem::Version
163
+ version: '0'
164
+ required_rubygems_version: !ruby/object:Gem::Requirement
165
+ requirements:
166
+ - - ">="
167
+ - !ruby/object:Gem::Version
168
+ version: '0'
169
+ requirements: []
170
+ rubygems_version: 3.0.6
171
+ signing_key:
172
+ specification_version: 4
173
+ summary: Easily sync your Android keystores across your team
174
+ test_files: []