fastlane-plugin-match_keystore 0.1.16 → 0.2.1
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:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 13f57b6db57c10e142d53885360cb1416f676402fd7f234b61a549720501d9c6
|
4
|
+
data.tar.gz: 2809854cf748f5ba4931fd4cbe8dfc6292cfea2f4e5d556a887ed422ebfcacc2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 97a47758d5a39ebf414ae7a232e8767cb0dfa6ac7c484f3180b8e3149d3e879fb3ce3b27d363eae444703e9f3a9bad5b4ddc5d66f2987ed5fa0ed29b04bd4c74
|
7
|
+
data.tar.gz: 2c27e6a39112bdef3c44c6ed8abd77a31e19c9ce52a002040562d9f4aee229b4cf0e60cbe868196493acc983ca61b39acdfadebd2914e5a9ba15d40dd1024751
|
data/README.md
CHANGED
@@ -2,6 +2,13 @@
|
|
2
2
|
|
3
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
4
|
|
5
|
+
## Machine requirements
|
6
|
+
|
7
|
+
* OpenSSL 1.1.1 min OR LibreSSL 2.9 min installed
|
8
|
+
* Git installed
|
9
|
+
* Android SDK & Build-tools installed
|
10
|
+
* ANDROID_HOME environment variable defined
|
11
|
+
|
5
12
|
## Getting Started
|
6
13
|
|
7
14
|
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:
|
@@ -43,6 +50,8 @@ The keystore properties are encrypted with AES in order to secure sensitive data
|
|
43
50
|
end
|
44
51
|
```
|
45
52
|
|
53
|
+
You can build aab files as well by providing an `aab_path` instead of an `apk_path`.
|
54
|
+
|
46
55
|
## Example
|
47
56
|
|
48
57
|
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`.
|
@@ -2,6 +2,7 @@ require 'fastlane/action'
|
|
2
2
|
require 'fileutils'
|
3
3
|
require 'os'
|
4
4
|
require 'json'
|
5
|
+
require 'pry'
|
5
6
|
require 'digest'
|
6
7
|
require_relative '../helper/match_keystore_helper'
|
7
8
|
|
@@ -11,14 +12,13 @@ module Fastlane
|
|
11
12
|
MATCH_KEYSTORE_PATH = :MATCH_KEYSTORE_PATH
|
12
13
|
MATCH_KEYSTORE_ALIAS_NAME = :MATCH_KEYSTORE_ALIAS_NAME
|
13
14
|
MATCH_KEYSTORE_APK_SIGNED = :MATCH_KEYSTORE_APK_SIGNED
|
15
|
+
MATCH_KEYSTORE_AAB_SIGNED = :MATCH_KEYSTORE_AAB_SIGNED
|
14
16
|
end
|
15
17
|
|
16
18
|
class MatchKeystoreAction < Action
|
17
19
|
|
18
|
-
|
19
|
-
|
20
|
-
path
|
21
|
-
end
|
20
|
+
KEY_VERSION = "2"
|
21
|
+
OPENSSL_BIN_PATH_MAC = "/usr/local/opt/openssl@1.1/bin"
|
22
22
|
|
23
23
|
def self.to_md5(value)
|
24
24
|
hash_value = Digest::MD5.hexdigest value
|
@@ -119,8 +119,7 @@ module Fastlane
|
|
119
119
|
|
120
120
|
def self.openssl(forceOpenSSL)
|
121
121
|
if forceOpenSSL
|
122
|
-
|
123
|
-
output = "#{path}/openssl"
|
122
|
+
output = "#{self::OPENSSL_BIN_PATH_MAC}/openssl"
|
124
123
|
else
|
125
124
|
output = "openssl"
|
126
125
|
end
|
@@ -137,10 +136,15 @@ module Fastlane
|
|
137
136
|
result
|
138
137
|
end
|
139
138
|
|
140
|
-
def self.gen_key(key_path, password)
|
139
|
+
def self.gen_key(key_path, password, compat_key)
|
141
140
|
`rm -f '#{key_path}'`
|
142
141
|
shaValue = self.sha512(password)
|
143
|
-
|
142
|
+
# Backward-compatibility
|
143
|
+
if compat_key == "1"
|
144
|
+
`echo "#{password}" | openssl dgst -sha512 | awk '{print $2}' | cut -c1-128 > '#{key_path}'`
|
145
|
+
else
|
146
|
+
`echo "#{shaValue}" > '#{key_path}'`
|
147
|
+
end
|
144
148
|
end
|
145
149
|
|
146
150
|
def self.encrypt_file(clear_file, encrypt_file, key_path, forceOpenSSL)
|
@@ -190,7 +194,7 @@ module Fastlane
|
|
190
194
|
|
191
195
|
# Check SHA-512-File
|
192
196
|
key_path = File.join(Dir.pwd, '/temp/key.txt')
|
193
|
-
self.gen_key(key_path, fakeValue)
|
197
|
+
self.gen_key(key_path, fakeValue, false)
|
194
198
|
shaValue = self.get_file_content(key_path).strip!
|
195
199
|
excepted = "cc6a7b0d89cc61c053f7018a305672bdb82bc07e5015f64bb063d9662be4ec81ec8afa819b009de266482b6bd56b7068def2524c32f5b5d4d9db49ee4578499d"
|
196
200
|
self.assert_equals("SHA-512-File", excepted, shaValue)
|
@@ -250,49 +254,68 @@ module Fastlane
|
|
250
254
|
build_tools_path = self.get_build_tools(version_targeted)
|
251
255
|
UI.message("Build-tools path: #{build_tools_path}")
|
252
256
|
|
253
|
-
# https://developer.android.com/studio/command-line/
|
254
|
-
if zip_align == true
|
255
|
-
apk_path_aligned = apk_path.gsub(".apk", "-aligned.apk")
|
256
|
-
`rm -f '#{apk_path_aligned}'`
|
257
|
-
UI.message("Aligning APK (zipalign): #{apk_path}")
|
258
|
-
output = `#{build_tools_path}zipalign -v 4 '#{apk_path}' '#{apk_path_aligned}'`
|
259
|
-
puts ""
|
260
|
-
puts output
|
261
|
-
|
262
|
-
if !File.file?(apk_path_aligned)
|
263
|
-
raise "Aligned APK not exists!"
|
264
|
-
end
|
265
|
-
|
266
|
-
else
|
267
|
-
UI.message("No zip align!")
|
268
|
-
apk_path_aligned = apk_path
|
269
|
-
end
|
257
|
+
# https://developer.android.com/studio/command-line/apksigner
|
270
258
|
apk_path_signed = apk_path.gsub(".apk", "-signed.apk")
|
271
259
|
apk_path_signed = apk_path_signed.gsub("unsigned", "")
|
272
260
|
apk_path_signed = apk_path_signed.gsub("--", "-")
|
273
|
-
|
274
|
-
# https://developer.android.com/studio/command-line/apksigner
|
275
261
|
`rm -f '#{apk_path_signed}'`
|
276
|
-
|
262
|
+
|
263
|
+
UI.message("Signing APK (input): #{apk_path}")
|
277
264
|
apksigner_opts = ""
|
278
265
|
build_tools_version = self.get_build_tools_version(version_targeted)
|
279
266
|
UI.message("Build-tools version: #{build_tools_version}")
|
280
267
|
if Gem::Version.new(build_tools_version) >= Gem::Version.new('30')
|
281
268
|
apksigner_opts = "--v4-signing-enabled false "
|
282
269
|
end
|
283
|
-
output = `#{build_tools_path}apksigner sign --ks '#{keystore_path}' --ks-key-alias '#{alias_name}' --ks-pass pass:'#{key_password}' --key-pass pass:'#{alias_password}' --v1-signing-enabled true --v2-signing-enabled true #{apksigner_opts}--out '#{apk_path_signed}' '#{
|
270
|
+
output = `#{build_tools_path}apksigner sign --ks '#{keystore_path}' --ks-key-alias '#{alias_name}' --ks-pass pass:'#{key_password}' --key-pass pass:'#{alias_password}' --v1-signing-enabled true --v2-signing-enabled true #{apksigner_opts}--out '#{apk_path_signed}' '#{apk_path}'`
|
284
271
|
puts ""
|
285
272
|
puts output
|
286
273
|
|
287
|
-
UI.message("Verifing APK signature: #{apk_path_signed}")
|
274
|
+
UI.message("Verifing APK signature (output): #{apk_path_signed}")
|
288
275
|
output = `#{build_tools_path}apksigner verify '#{apk_path_signed}'`
|
289
276
|
puts ""
|
290
277
|
puts output
|
291
|
-
|
278
|
+
|
279
|
+
|
280
|
+
# https://developer.android.com/studio/command-line/zipalign
|
281
|
+
if zip_align != false
|
282
|
+
apk_path_aligned = apk_path_signed.gsub(".apk", "-aligned.apk")
|
283
|
+
`rm -f '#{apk_path_aligned}'`
|
284
|
+
UI.message("Aligning APK (zipalign): #{apk_path_signed}")
|
285
|
+
output = `#{build_tools_path}zipalign -v 4 '#{apk_path_signed}' '#{apk_path_aligned}'`
|
286
|
+
puts ""
|
287
|
+
puts output
|
288
|
+
|
289
|
+
if !File.file?(apk_path_aligned)
|
290
|
+
raise "Aligned APK not exists!"
|
291
|
+
end
|
292
|
+
|
293
|
+
`rm -f '#{apk_path_signed}'`
|
294
|
+
apk_path_signed = apk_path_aligned
|
295
|
+
|
296
|
+
else
|
297
|
+
UI.message("No zip align - deactivated via parameter!")
|
298
|
+
end
|
292
299
|
|
293
300
|
apk_path_signed
|
294
301
|
end
|
295
302
|
|
303
|
+
def self.sign_aab(aab_path, keystore_path, key_password, alias_name, alias_password)
|
304
|
+
|
305
|
+
aab_path_signed = aab_path.gsub('.aab', '-signed.aab')
|
306
|
+
aab_path_signed = aab_path_signed.gsub('unsigned', '')
|
307
|
+
aab_path_signed = aab_path_signed.gsub('--', '-')
|
308
|
+
`rm -f '#{aab_path_signed}'`
|
309
|
+
|
310
|
+
UI.message("Signing AAB (input): #{aab_path}")
|
311
|
+
aabsigner_opts = ""
|
312
|
+
output = `jarsigner -keystore '#{keystore_path}' -storepass '#{key_password}' -keypass '#{alias_password}' -signedjar '#{aab_path_signed}' '#{aab_path}' '#{alias_name}'`
|
313
|
+
puts ""
|
314
|
+
puts output
|
315
|
+
|
316
|
+
aab_path_signed
|
317
|
+
end
|
318
|
+
|
296
319
|
def self.resolve_dir(path)
|
297
320
|
if !File.directory?(path)
|
298
321
|
path = File.join(Dir.pwd, path)
|
@@ -316,6 +339,34 @@ module Fastlane
|
|
316
339
|
data
|
317
340
|
end
|
318
341
|
|
342
|
+
def self.resolve_aab_path(aab_path)
|
343
|
+
|
344
|
+
# Set default AAB path if not set:
|
345
|
+
if aab_path.to_s.strip.empty?
|
346
|
+
aab_path = '/app/build/outputs/bundle/release/'
|
347
|
+
end
|
348
|
+
|
349
|
+
if !aab_path.to_s.end_with?('.aab')
|
350
|
+
|
351
|
+
aab_path = self.resolve_dir(aab_path)
|
352
|
+
|
353
|
+
pattern = File.join(aab_path, '*.aab')
|
354
|
+
files = Dir[pattern]
|
355
|
+
|
356
|
+
for file in files
|
357
|
+
if file.to_s.end_with?('.aab') && !file.to_s.end_with?("-signed.aab")
|
358
|
+
apk_path = file
|
359
|
+
break
|
360
|
+
end
|
361
|
+
end
|
362
|
+
|
363
|
+
else
|
364
|
+
aab_path = self.resolve_file(aab_path)
|
365
|
+
end
|
366
|
+
|
367
|
+
aab_path
|
368
|
+
end
|
369
|
+
|
319
370
|
def self.resolve_apk_path(apk_path)
|
320
371
|
|
321
372
|
# Set default APK path if not set:
|
@@ -360,6 +411,7 @@ module Fastlane
|
|
360
411
|
git_url = params[:git_url]
|
361
412
|
package_name = params[:package_name]
|
362
413
|
apk_path = params[:apk_path]
|
414
|
+
aab_path = params[:aab_path]
|
363
415
|
existing_keystore = params[:existing_keystore]
|
364
416
|
match_secret = params[:match_secret]
|
365
417
|
override_keystore = params[:override_keystore]
|
@@ -367,6 +419,8 @@ module Fastlane
|
|
367
419
|
clear_keystore = params[:clear_keystore]
|
368
420
|
unit_test = params[:unit_test]
|
369
421
|
build_tools_version = params[:build_tools_version]
|
422
|
+
zip_align = params[:zip_align]
|
423
|
+
compat_key = params[:compat_key]
|
370
424
|
|
371
425
|
# Test OpenSSL/LibreSSL
|
372
426
|
if unit_test
|
@@ -390,6 +444,11 @@ module Fastlane
|
|
390
444
|
# Check OpenSSL:
|
391
445
|
self.check_ssl_version(false)
|
392
446
|
|
447
|
+
# Check is backward-compatibility is required:
|
448
|
+
if !compat_key.to_s.strip.empty?
|
449
|
+
UI.message("Compatiblity version: #{compat_key}")
|
450
|
+
end
|
451
|
+
|
393
452
|
# Init workign local directory:
|
394
453
|
dir_name = ENV['HOME'] + '/.match_keystore'
|
395
454
|
unless File.directory?(dir_name)
|
@@ -398,7 +457,11 @@ module Fastlane
|
|
398
457
|
end
|
399
458
|
|
400
459
|
# Init 'security password' for AES encryption:
|
401
|
-
|
460
|
+
if compat_key == "1"
|
461
|
+
key_name = "#{self.to_md5(git_url)}.hex"
|
462
|
+
else
|
463
|
+
key_name = "#{self.to_md5(git_url)}-#{self::KEY_VERSION}.hex"
|
464
|
+
end
|
402
465
|
key_path = File.join(dir_name, key_name)
|
403
466
|
# UI.message(key_path)
|
404
467
|
if !File.file?(key_path)
|
@@ -407,7 +470,7 @@ module Fastlane
|
|
407
470
|
raise "Security password is not defined! Please use 'match_secret' parameter for CI."
|
408
471
|
end
|
409
472
|
UI.message "Generating security key '#{key_name}'..."
|
410
|
-
self.gen_key(key_path, security_password)
|
473
|
+
self.gen_key(key_path, security_password, compat_key)
|
411
474
|
end
|
412
475
|
|
413
476
|
# Check is 'security password' is well initialized:
|
@@ -446,14 +509,10 @@ module Fastlane
|
|
446
509
|
gitDir = File.join(repo_dir, '/.git')
|
447
510
|
if !File.directory?(gitDir)
|
448
511
|
UI.message("Cloning remote Keystores repository...")
|
449
|
-
puts ''
|
450
512
|
`git clone #{git_url} #{repo_dir}`
|
451
|
-
puts ''
|
452
513
|
else
|
453
514
|
UI.message("Pulling remote Keystores repository...")
|
454
|
-
puts ''
|
455
515
|
`cd #{repo_dir} && git pull`
|
456
|
-
puts ''
|
457
516
|
end
|
458
517
|
|
459
518
|
# Load parameters from JSON for CI or Unit Tests:
|
@@ -554,6 +613,7 @@ module Fastlane
|
|
554
613
|
self.decrypt_file(properties_encrypt_path, properties_path, key_path, false)
|
555
614
|
|
556
615
|
properties = self.load_properties(properties_path)
|
616
|
+
# Pry::ColorPrinter.pp(properties)
|
557
617
|
key_password = properties['keyPassword']
|
558
618
|
alias_name = properties['aliasName']
|
559
619
|
alias_password = properties['aliasPassword']
|
@@ -561,13 +621,13 @@ module Fastlane
|
|
561
621
|
File.delete(properties_path)
|
562
622
|
end
|
563
623
|
|
564
|
-
# Resolve path to the APK to sign:
|
565
|
-
output_signed_apk = ''
|
566
|
-
apk_path = self.resolve_apk_path(apk_path)
|
567
|
-
|
568
624
|
# Sign APK:
|
569
|
-
if File.file?(apk_path)
|
625
|
+
if apk_path && File.file?(apk_path)
|
570
626
|
UI.message("APK to sign: " + apk_path)
|
627
|
+
|
628
|
+
# Resolve path to the APK to sign:
|
629
|
+
output_signed_apk = ''
|
630
|
+
apk_path = self.resolve_apk_path(apk_path)
|
571
631
|
|
572
632
|
if File.file?(keystore_path)
|
573
633
|
|
@@ -579,21 +639,49 @@ module Fastlane
|
|
579
639
|
key_password,
|
580
640
|
alias_name,
|
581
641
|
alias_password,
|
582
|
-
|
642
|
+
zip_align, # Zip align
|
583
643
|
build_tools_version # Buil-tools version
|
584
644
|
)
|
585
645
|
puts ''
|
586
646
|
end
|
587
|
-
else
|
588
|
-
UI.message("No APK file found to sign!")
|
589
|
-
end
|
590
647
|
|
591
|
-
|
592
|
-
|
593
|
-
|
594
|
-
|
648
|
+
# Prepare contect shared values for next lanes:
|
649
|
+
Actions.lane_context[SharedValues::MATCH_KEYSTORE_PATH] = keystore_path
|
650
|
+
Actions.lane_context[SharedValues::MATCH_KEYSTORE_ALIAS_NAME] = alias_name
|
651
|
+
Actions.lane_context[SharedValues::MATCH_KEYSTORE_APK_SIGNED] = output_signed_apk
|
652
|
+
|
653
|
+
output_signed_apk
|
654
|
+
# Sign AAB
|
655
|
+
elsif aab_path && File.file?(aab_path)
|
656
|
+
UI.message('AAB to sign: '+ aab_path)
|
657
|
+
|
658
|
+
# Resolve path to the AAB to sign:
|
659
|
+
output_signed_aab = ''
|
660
|
+
aab_path = self.resolve_aab_path(aab_path)
|
661
|
+
|
662
|
+
if File.file?(keystore_path)
|
663
|
+
|
664
|
+
UI.message("Signing the AAB...")
|
665
|
+
puts ''
|
666
|
+
output_signed_aab = self.sign_aab(
|
667
|
+
aab_path,
|
668
|
+
keystore_path,
|
669
|
+
key_password,
|
670
|
+
alias_name,
|
671
|
+
alias_password
|
672
|
+
)
|
673
|
+
puts ''
|
674
|
+
end
|
675
|
+
|
676
|
+
# Prepare contect shared values for next lanes:
|
677
|
+
Actions.lane_context[SharedValues::MATCH_KEYSTORE_PATH] = keystore_path
|
678
|
+
Actions.lane_context[SharedValues::MATCH_KEYSTORE_ALIAS_NAME] = alias_name
|
679
|
+
Actions.lane_context[SharedValues::MATCH_KEYSTORE_AAB_SIGNED] = output_signed_aab
|
595
680
|
|
596
|
-
|
681
|
+
output_signed_aab
|
682
|
+
else
|
683
|
+
UI.message("No APK or AAB file found")
|
684
|
+
end
|
597
685
|
end
|
598
686
|
|
599
687
|
def self.description
|
@@ -601,7 +689,7 @@ module Fastlane
|
|
601
689
|
end
|
602
690
|
|
603
691
|
def self.authors
|
604
|
-
["Christopher NEY"]
|
692
|
+
["Christopher NEY", "Simon Scherzinger"]
|
605
693
|
end
|
606
694
|
|
607
695
|
def self.return_value
|
@@ -612,7 +700,8 @@ module Fastlane
|
|
612
700
|
[
|
613
701
|
['MATCH_KEYSTORE_PATH', 'File path of the Keystore fot the App.'],
|
614
702
|
['MATCH_KEYSTORE_ALIAS_NAME', 'Keystore Alias Name.'],
|
615
|
-
['MATCH_KEYSTORE_APK_SIGNED', 'Path of the signed APK.']
|
703
|
+
['MATCH_KEYSTORE_APK_SIGNED', 'Path of the signed APK.'],
|
704
|
+
['MATCH_KEYSTORE_AAB_SIGNED', 'Path of the signed AAB.']
|
616
705
|
]
|
617
706
|
end
|
618
707
|
|
@@ -638,6 +727,11 @@ module Fastlane
|
|
638
727
|
description: "Path of the APK file to sign",
|
639
728
|
optional: true,
|
640
729
|
type: String),
|
730
|
+
FastlaneCore::ConfigItem.new(key: :aab_path,
|
731
|
+
env_name: "MATCH_KEYSTORE_AAB_PATH",
|
732
|
+
description: "Path of the AAB file to sign",
|
733
|
+
optional: true,
|
734
|
+
type: String),
|
641
735
|
FastlaneCore::ConfigItem.new(key: :match_secret,
|
642
736
|
env_name: "MATCH_KEYSTORE_SECRET",
|
643
737
|
description: "Secret to decrypt keystore.properties file (CI)",
|
@@ -662,17 +756,27 @@ module Fastlane
|
|
662
756
|
env_name: "MATCH_KEYSTORE_BUILD_TOOLS_VERSION",
|
663
757
|
description: "Set built-tools version (by default latest available on machine)",
|
664
758
|
optional: true,
|
665
|
-
type: String),
|
759
|
+
type: String),
|
760
|
+
FastlaneCore::ConfigItem.new(key: :zip_align,
|
761
|
+
env_name: "MATCH_KEYSTORE_ZIPALIGN",
|
762
|
+
description: "Define if plugin will run zipalign on APK before sign it (true by default)",
|
763
|
+
optional: true,
|
764
|
+
type: Boolean),
|
765
|
+
FastlaneCore::ConfigItem.new(key: :compat_key,
|
766
|
+
env_name: "MATCH_KEYSTORE_COMPAT_KEY",
|
767
|
+
description: "Define the compatibility key version used on local machine (nil by default)",
|
768
|
+
optional: true,
|
769
|
+
type: String),
|
666
770
|
FastlaneCore::ConfigItem.new(key: :clear_keystore,
|
667
771
|
env_name: "MATCH_KEYSTORE_CLEAR",
|
668
772
|
description: "Clear the local keystore (false by default)",
|
669
773
|
optional: true,
|
670
774
|
type: Boolean),
|
671
775
|
FastlaneCore::ConfigItem.new(key: :unit_test,
|
672
|
-
|
776
|
+
env_name: "MATCH_KEYSTORE_UNIT_TESTS",
|
673
777
|
description: "launch Unit Tests (false by default)",
|
674
|
-
|
675
|
-
|
778
|
+
optional: true,
|
779
|
+
type: Boolean)
|
676
780
|
]
|
677
781
|
end
|
678
782
|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: fastlane-plugin-match_keystore
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1
|
4
|
+
version: 0.2.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Christopher NEY
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2021-09-15 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: pry
|
@@ -167,7 +167,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
167
167
|
- !ruby/object:Gem::Version
|
168
168
|
version: '0'
|
169
169
|
requirements: []
|
170
|
-
rubygems_version: 3.
|
170
|
+
rubygems_version: 3.2.17
|
171
171
|
signing_key:
|
172
172
|
specification_version: 4
|
173
173
|
summary: Easily sync your Android keystores across your team
|