fastlane 2.219.0 → 2.221.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (94) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +86 -86
  3. data/bin/console +11 -0
  4. data/bin/match_file +60 -0
  5. data/deliver/lib/deliver/download_screenshots.rb +2 -1
  6. data/deliver/lib/deliver/generate_summary.rb +1 -1
  7. data/deliver/lib/deliver/options.rb +14 -0
  8. data/deliver/lib/deliver/runner.rb +4 -4
  9. data/deliver/lib/deliver/submit_for_review.rb +2 -2
  10. data/deliver/lib/deliver/upload_metadata.rb +43 -28
  11. data/deliver/lib/deliver/upload_screenshots.rb +15 -8
  12. data/fastlane/lib/fastlane/actions/app_store_connect_api_key.rb +2 -1
  13. data/fastlane/lib/fastlane/actions/appetize.rb +4 -0
  14. data/fastlane/lib/fastlane/actions/docs/sync_code_signing.md +6 -2
  15. data/fastlane/lib/fastlane/actions/git_add.rb +17 -2
  16. data/fastlane/lib/fastlane/actions/mailgun.rb +30 -8
  17. data/fastlane/lib/fastlane/actions/onesignal.rb +14 -2
  18. data/fastlane/lib/fastlane/actions/spm.rb +7 -0
  19. data/fastlane/lib/fastlane/actions/update_project_provisioning.rb +2 -1
  20. data/fastlane/lib/fastlane/actions/upload_symbols_to_sentry.rb +4 -2
  21. data/fastlane/lib/fastlane/commands_generator.rb +9 -0
  22. data/fastlane/lib/fastlane/console.rb +24 -0
  23. data/fastlane/lib/fastlane/helper/sh_helper.rb +1 -1
  24. data/fastlane/lib/fastlane/lane_manager_base.rb +16 -8
  25. data/fastlane/lib/fastlane/plugins/plugin_fetcher.rb +2 -1
  26. data/fastlane/lib/fastlane/plugins/plugin_info_collector.rb +2 -1
  27. data/fastlane/lib/fastlane/plugins/plugin_manager.rb +2 -1
  28. data/fastlane/lib/fastlane/runner.rb +2 -2
  29. data/fastlane/lib/fastlane/version.rb +2 -1
  30. data/fastlane/swift/Deliverfile.swift +1 -1
  31. data/fastlane/swift/DeliverfileProtocol.swift +9 -1
  32. data/fastlane/swift/Fastlane.swift +48 -11
  33. data/fastlane/swift/Gymfile.swift +1 -1
  34. data/fastlane/swift/GymfileProtocol.swift +1 -1
  35. data/fastlane/swift/LaneFileProtocol.swift +1 -1
  36. data/fastlane/swift/Matchfile.swift +1 -1
  37. data/fastlane/swift/MatchfileProtocol.swift +1 -1
  38. data/fastlane/swift/OptionalConfigValue.swift +2 -2
  39. data/fastlane/swift/Precheckfile.swift +1 -1
  40. data/fastlane/swift/PrecheckfileProtocol.swift +1 -1
  41. data/fastlane/swift/Scanfile.swift +1 -1
  42. data/fastlane/swift/ScanfileProtocol.swift +1 -1
  43. data/fastlane/swift/Screengrabfile.swift +1 -1
  44. data/fastlane/swift/ScreengrabfileProtocol.swift +1 -1
  45. data/fastlane/swift/Snapshotfile.swift +1 -1
  46. data/fastlane/swift/SnapshotfileProtocol.swift +3 -3
  47. data/fastlane/swift/formatting/Brewfile.lock.json +19 -19
  48. data/fastlane_core/lib/fastlane_core/cert_checker.rb +11 -8
  49. data/fastlane_core/lib/fastlane_core/device_manager.rb +1 -1
  50. data/fastlane_core/lib/fastlane_core/helper.rb +0 -15
  51. data/fastlane_core/lib/fastlane_core/print_table.rb +16 -0
  52. data/fastlane_core/lib/fastlane_core/project.rb +5 -0
  53. data/fastlane_core/lib/fastlane_core/ui/fastlane_runner.rb +0 -4
  54. data/fastlane_core/lib/fastlane_core/ui/help_formatter.rb +1 -5
  55. data/fastlane_core/lib/fastlane_core/ui/implementations/shell.rb +2 -0
  56. data/frameit/lib/frameit/device_types.rb +4 -0
  57. data/frameit/lib/frameit/editor.rb +20 -0
  58. data/gym/lib/gym/detect_values.rb +2 -0
  59. data/gym/lib/gym/module.rb +2 -2
  60. data/match/lib/assets/READMETemplate.md +3 -5
  61. data/match/lib/match/encryption/encryption.rb +154 -0
  62. data/match/lib/match/encryption/openssl.rb +7 -38
  63. data/match/lib/match/encryption.rb +1 -0
  64. data/match/lib/match/runner.rb +44 -6
  65. data/match/lib/match/storage/git_storage.rb +4 -3
  66. data/match/lib/match/storage/interface.rb +9 -5
  67. data/pilot/lib/pilot/build_manager.rb +14 -6
  68. data/pilot/lib/pilot/manager.rb +2 -2
  69. data/pilot/lib/pilot/options.rb +1 -1
  70. data/snapshot/lib/snapshot/options.rb +2 -2
  71. data/snapshot/lib/snapshot/setup.rb +1 -1
  72. data/spaceship/lib/spaceship/connect_api/api_client.rb +2 -2
  73. data/spaceship/lib/spaceship/connect_api/models/app.rb +28 -33
  74. data/spaceship/lib/spaceship/connect_api/models/app_info.rb +17 -0
  75. data/spaceship/lib/spaceship/connect_api/models/app_store_version.rb +44 -9
  76. data/spaceship/lib/spaceship/connect_api/models/beta_tester.rb +30 -2
  77. data/spaceship/lib/spaceship/connect_api/models/certificate.rb +2 -2
  78. data/spaceship/lib/spaceship/connect_api/models/device.rb +11 -6
  79. data/spaceship/lib/spaceship/connect_api/models/profile.rb +8 -1
  80. data/spaceship/lib/spaceship/connect_api/provisioning/client.rb +1 -1
  81. data/spaceship/lib/spaceship/connect_api/provisioning/provisioning.rb +31 -22
  82. data/spaceship/lib/spaceship/connect_api/testflight/client.rb +1 -1
  83. data/spaceship/lib/spaceship/connect_api/testflight/testflight.rb +47 -43
  84. data/spaceship/lib/spaceship/connect_api/token.rb +9 -3
  85. data/spaceship/lib/spaceship/connect_api/tunes/client.rb +1 -1
  86. data/spaceship/lib/spaceship/connect_api/tunes/tunes.rb +96 -90
  87. data/spaceship/lib/spaceship/connect_api/users/client.rb +1 -1
  88. data/spaceship/lib/spaceship/connect_api/users/users.rb +15 -11
  89. data/spaceship/lib/spaceship/connect_api.rb +5 -2
  90. data/spaceship/lib/spaceship/portal/certificate.rb +2 -2
  91. data/spaceship/lib/spaceship/portal/provisioning_profile.rb +8 -1
  92. data/spaceship/lib/spaceship/stats_middleware.rb +2 -2
  93. data/trainer/lib/trainer/xcresult.rb +6 -10
  94. metadata +50 -33
@@ -1,6 +1,22 @@
1
1
  require_relative 'configuration/configuration'
2
2
  require_relative 'helper'
3
3
 
4
+ # Monkey patch Terminal::Table until this is merged
5
+ # https://github.com/tj/terminal-table/pull/131
6
+ # solves https://github.com/fastlane/fastlane/issues/21852
7
+ # loads Terminal::Table first to be able to monkey patch it.
8
+ require 'terminal-table'
9
+ module Terminal
10
+ class Table
11
+ class Cell
12
+ def lines
13
+ # @value.to_s.split(/\n/)
14
+ @value.to_s.encode("utf-8", invalid: :replace).split(/\n/)
15
+ end
16
+ end
17
+ end
18
+ end
19
+
4
20
  module FastlaneCore
5
21
  class PrintTable
6
22
  class << self
@@ -307,6 +307,10 @@ module FastlaneCore
307
307
  supported_platforms.include?(:watchOS)
308
308
  end
309
309
 
310
+ def visionos?
311
+ supported_platforms.include?(:visionOS)
312
+ end
313
+
310
314
  def multiplatform?
311
315
  supported_platforms.count > 1
312
316
  end
@@ -323,6 +327,7 @@ module FastlaneCore
323
327
  when "iphonesimulator", "iphoneos" then :iOS
324
328
  when "watchsimulator", "watchos" then :watchOS
325
329
  when "appletvsimulator", "appletvos" then :tvOS
330
+ when "xros", "xrsimulator" then :visionOS
326
331
  end
327
332
  end.uniq.compact
328
333
  end
@@ -243,10 +243,6 @@ module Commander
243
243
  ui.error(e.to_s)
244
244
  ui.error("")
245
245
  ui.error("SSL errors can be caused by various components on your local machine.")
246
- if Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.1')
247
- ui.error("Apple has recently changed their servers to require TLS 1.2, which may")
248
- ui.error("not be available to your system installed Ruby (#{RUBY_VERSION})")
249
- end
250
246
  ui.error("")
251
247
  ui.error("The best solution is to use the self-contained fastlane version.")
252
248
  ui.error("Which ships with a bundled OpenSSL,ruby and all gems - so you don't depend on system libraries")
@@ -6,11 +6,7 @@ module FastlaneCore
6
6
  # fastlane only customizes the global command help
7
7
  return super unless name == :help
8
8
 
9
- if Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.6')
10
- ERB.new(File.read(File.join(File.dirname(__FILE__), "help.erb")), nil, '-')
11
- else
12
- ERB.new(File.read(File.join(File.dirname(__FILE__), "help.erb")), trim_mode: '-')
13
- end
9
+ ERB.new(File.read(File.join(File.dirname(__FILE__), "help.erb")), trim_mode: '-')
14
10
  end
15
11
  end
16
12
  end
@@ -108,12 +108,14 @@ module FastlaneCore
108
108
  start_line = error_line - 2 < 1 ? 1 : error_line - 2
109
109
  end_line = error_line + 2 < contents.length ? error_line + 2 : contents.length
110
110
 
111
+ error('```')
111
112
  Range.new(start_line, end_line).each do |line|
112
113
  str = line == error_line ? " => " : " "
113
114
  str << line.to_s.rjust(Math.log10(end_line) + 1)
114
115
  str << ":\t#{contents[line - 1]}"
115
116
  error(str)
116
117
  end
118
+ error('```')
117
119
  end
118
120
 
119
121
  #####################################################
@@ -133,6 +133,10 @@ module Frameit
133
133
  IPHONE_13_PRO ||= Frameit::Device.new("iphone-13-pro", "Apple iPhone 13 Pro", 11, [[1170, 2532], [2532, 1170]], 460, Color::GRAPHITE, Platform::IOS)
134
134
  IPHONE_13_PRO_MAX ||= Frameit::Device.new("iphone13-pro-max", "Apple iPhone 13 Pro Max", 11, [[1284, 2778], [2778, 1284]], 458, Color::GRAPHITE, Platform::IOS)
135
135
  IPHONE_13_MINI ||= Frameit::Device.new("iphone-13-mini", "Apple iPhone 13 Mini", 11, [[1080, 2340], [2340, 1080]], 476, Color::MIDNIGHT, Platform::IOS)
136
+ IPHONE_14 ||= Frameit::Device.new("iphone-14", "Apple iPhone 14", 12, [[1170, 2532], [2532, 1170]], 460, Color::MIDNIGHT, Platform::IOS)
137
+ IPHONE_14_PLUS ||= Frameit::Device.new("iphone-14-plus", "Apple iPhone 14 Plus", 12, [[1284, 2778], [2778, 1284]], 458, Color::MIDNIGHT, Platform::IOS)
138
+ IPHONE_14_PRO ||= Frameit::Device.new("iphone-14-pro", "Apple iPhone 14 Pro", 12, [[1178, 2556], [2556, 1178]], 460, Color::PURPLE, Platform::IOS)
139
+ IPHONE_14_PRO_MAX ||= Frameit::Device.new("iphone14-pro-max", "Apple iPhone 14 Pro Max", 12, [[1290, 2796], [2796, 1290]], 458, Color::PURPLE, Platform::IOS)
136
140
  IPAD_10_2 ||= Frameit::Device.new("ipad-10-2", "Apple iPad 10.2", 1, [[1620, 2160], [2160, 1620]], 264, Color::SPACE_GRAY, Platform::IOS)
137
141
  IPAD_AIR_2 ||= Frameit::Device.new("ipad-air-2", "Apple iPad Air 2", 1, [[1536, 2048], [2048, 1536]], 264, Color::SPACE_GRAY, Platform::IOS, Deliver::AppScreenshot::ScreenSize::IOS_IPAD)
138
142
  IPAD_AIR_2019 ||= Frameit::Device.new("ipad-air-2019", "Apple iPad Air (2019)", 2, [[1668, 2224], [2224, 1668]], 265, Color::SPACE_GRAY, Platform::IOS)
@@ -109,6 +109,26 @@ module Frameit
109
109
  end
110
110
  end
111
111
 
112
+ # Apply rounded corners for all iPhone 14 devices
113
+ if screenshot.device.id.to_s.include?("iphone-14") || screenshot.device.id.to_s.include?("iphone14")
114
+
115
+ maskData = MiniMagick::Tool::Convert.new do |img|
116
+ img.size("#{screenshot.size[0]}x#{screenshot.size[1]}")
117
+ img.canvas('none')
118
+ img.draw("roundrectangle 0,0,#{screenshot.size[0]},#{screenshot.size[1]},100,100")
119
+ img << 'png:-'
120
+ end
121
+
122
+ # Create a mask
123
+ mask = MiniMagick::Image.read(maskData)
124
+
125
+ @image = @image.composite(mask, "png") do |c|
126
+ c.channel("A")
127
+ c.compose("DstIn")
128
+ c.alpha("on")
129
+ end
130
+ end
131
+
112
132
  @image = frame.composite(image, "png") do |c|
113
133
  c.compose("DstOver")
114
134
  c.geometry(offset['offset'])
@@ -147,6 +147,8 @@ module Gym
147
147
 
148
148
  platform = if Gym.project.tvos?
149
149
  "tvOS"
150
+ elsif Gym.project.visionos?
151
+ "visionOS"
150
152
  elsif Gym.building_for_ios?
151
153
  "iOS"
152
154
  elsif Gym.building_for_mac?
@@ -33,8 +33,8 @@ module Gym
33
33
  # Can be iOS project and build for mac if catalyst
34
34
  return false if building_mac_catalyst_for_mac?
35
35
 
36
- # Can be iOS project if iOS, tvOS, or watchOS
37
- return Gym.project.ios? || Gym.project.tvos? || Gym.project.watchos?
36
+ # Can be iOS project if iOS, tvOS, watchOS, or visionOS
37
+ return Gym.project.ios? || Gym.project.tvos? || Gym.project.watchos? || Gym.project.visionos?
38
38
  end
39
39
  end
40
40
 
@@ -14,13 +14,11 @@ Make sure you have the latest version of the Xcode command line tools installed:
14
14
  xcode-select --install
15
15
  ```
16
16
 
17
- Install _fastlane_ using
17
+ Install _fastlane_ using bundler by following instructions here on [fastlane docs](https://docs.fastlane.tools).
18
18
 
19
- ```
20
- [sudo] gem install fastlane -NV
21
- ```
19
+ or alternatively using
22
20
 
23
- or alternatively using `brew install fastlane`
21
+ `brew install fastlane`
24
22
 
25
23
  ### Usage
26
24
 
@@ -0,0 +1,154 @@
1
+ require 'base64'
2
+ require 'openssl'
3
+ require 'securerandom'
4
+
5
+ module Match
6
+ module Encryption
7
+ # This is to keep backwards compatibility with the old fastlane version which used the local openssl installation.
8
+ # The encryption parameters in this implementation reflect the old behavior which was the most common default value in those versions.
9
+ # As for decryption, 1.0.x OpenSSL and earlier versions use MD5, 1.1.0c and newer uses SHA256, we try both before giving an error
10
+ class EncryptionV1
11
+ ALGORITHM = 'aes-256-cbc'
12
+
13
+ def encrypt(data:, password:, salt:, hash_algorithm: "MD5")
14
+ cipher = ::OpenSSL::Cipher.new(ALGORITHM)
15
+ cipher.encrypt
16
+
17
+ keyivgen(cipher, password, salt, hash_algorithm)
18
+
19
+ encrypted_data = cipher.update(data)
20
+ encrypted_data << cipher.final
21
+ { encrypted_data: encrypted_data }
22
+ end
23
+
24
+ def decrypt(encrypted_data:, password:, salt:, hash_algorithm: "MD5")
25
+ cipher = ::OpenSSL::Cipher.new(ALGORITHM)
26
+ cipher.decrypt
27
+
28
+ keyivgen(cipher, password, salt, hash_algorithm)
29
+
30
+ data = cipher.update(encrypted_data)
31
+ data << cipher.final
32
+ end
33
+
34
+ private
35
+
36
+ def keyivgen(cipher, password, salt, hash_algorithm)
37
+ cipher.pkcs5_keyivgen(password, salt, 1, hash_algorithm)
38
+ end
39
+ end
40
+
41
+ # The newer encryption mechanism, which features a more secure key and IV generation.
42
+ #
43
+ # The IV is randomly generated and provided unencrypted.
44
+ # The salt should be randomly generated and provided unencrypted (like in the current implementation).
45
+ # The key is generated with OpenSSL::KDF::pbkdf2_hmac with properly chosen parameters.
46
+ #
47
+ # Short explanation about salt and IV: https://stackoverflow.com/a/1950674/6324550
48
+ class EncryptionV2
49
+ ALGORITHM = 'aes-256-gcm'
50
+
51
+ def encrypt(data:, password:, salt:)
52
+ cipher = ::OpenSSL::Cipher.new(ALGORITHM)
53
+ cipher.encrypt
54
+
55
+ keyivgen(cipher, password, salt)
56
+
57
+ encrypted_data = cipher.update(data)
58
+ encrypted_data << cipher.final
59
+
60
+ auth_tag = cipher.auth_tag
61
+
62
+ { encrypted_data: encrypted_data, auth_tag: auth_tag }
63
+ end
64
+
65
+ def decrypt(encrypted_data:, password:, salt:, auth_tag:)
66
+ cipher = ::OpenSSL::Cipher.new(ALGORITHM)
67
+ cipher.decrypt
68
+
69
+ keyivgen(cipher, password, salt)
70
+
71
+ cipher.auth_tag = auth_tag
72
+
73
+ data = cipher.update(encrypted_data)
74
+ data << cipher.final
75
+ end
76
+
77
+ private
78
+
79
+ def keyivgen(cipher, password, salt)
80
+ keyIv = ::OpenSSL::KDF.pbkdf2_hmac(password, salt: salt, iterations: 10_000, length: 32 + 12 + 24, hash: "sha256")
81
+ key = keyIv[0..31]
82
+ iv = keyIv[32..43]
83
+ auth_data = keyIv[44..-1]
84
+ cipher.key = key
85
+ cipher.iv = iv
86
+ cipher.auth_data = auth_data
87
+ end
88
+ end
89
+
90
+ class MatchDataEncryption
91
+ V1_PREFIX = "Salted__"
92
+ V2_PREFIX = "match_encrypted_v2__"
93
+
94
+ def encrypt(data:, password:, version: 2)
95
+ salt = SecureRandom.random_bytes(8)
96
+ if version == 2
97
+ e = EncryptionV2.new
98
+ encryption = e.encrypt(data: data, password: password, salt: salt)
99
+ encrypted_data = V2_PREFIX + salt + encryption[:auth_tag] + encryption[:encrypted_data]
100
+ else
101
+ e = EncryptionV1.new
102
+ encryption = e.encrypt(data: data, password: password, salt: salt)
103
+ encrypted_data = V1_PREFIX + salt + encryption[:encrypted_data]
104
+ end
105
+ Base64.encode64(encrypted_data)
106
+ end
107
+
108
+ def decrypt(base64encoded_encrypted:, password:)
109
+ stored_data = Base64.decode64(base64encoded_encrypted)
110
+ if stored_data.start_with?(V2_PREFIX)
111
+ salt = stored_data[20..27]
112
+ auth_tag = stored_data[28..43]
113
+ data_to_decrypt = stored_data[44..-1]
114
+ e = EncryptionV2.new
115
+ e.decrypt(encrypted_data: data_to_decrypt, password: password, salt: salt, auth_tag: auth_tag)
116
+ else
117
+ salt = stored_data[8..15]
118
+ data_to_decrypt = stored_data[16..-1]
119
+ e = EncryptionV1.new
120
+ begin
121
+ # Note that we are not guaranteed to catch the decryption errors here if the password or the hash is wrong
122
+ # as there's no integrity checks.
123
+ # see https://github.com/fastlane/fastlane/issues/21663
124
+ e.decrypt(encrypted_data: data_to_decrypt, password: password, salt: salt)
125
+ # With the wrong hash_algorithm, there's here 0.4% chance that the decryption failure will go undetected
126
+ rescue => _ex
127
+ # With a wrong password, there's a 0.4% chance it will decrypt garbage and not fail
128
+ fallback_hash_algorithm = "SHA256"
129
+ e.decrypt(encrypted_data: data_to_decrypt, password: password, salt: salt, hash_algorithm: fallback_hash_algorithm)
130
+ end
131
+ end
132
+ end
133
+ end
134
+
135
+ # The methods of this class will encrypt or decrypt files in place, by default.
136
+ class MatchFileEncryption
137
+ def encrypt(file_path:, password:, output_path: nil)
138
+ output_path = file_path unless output_path
139
+ data_to_encrypt = File.binread(file_path)
140
+ e = MatchDataEncryption.new
141
+ data = e.encrypt(data: data_to_encrypt, password: password)
142
+ File.write(output_path, data)
143
+ end
144
+
145
+ def decrypt(file_path:, password:, output_path: nil)
146
+ output_path = file_path unless output_path
147
+ content = File.read(file_path)
148
+ e = MatchDataEncryption.new
149
+ decrypted_data = e.decrypt(base64encoded_encrypted: content, password: password)
150
+ File.binwrite(output_path, decrypted_data)
151
+ end
152
+ end
153
+ end
154
+ end
@@ -109,25 +109,10 @@ module Match
109
109
  return password
110
110
  end
111
111
 
112
- # We encrypt with MD5 because that was the most common default value in older fastlane versions which used the local OpenSSL installation
113
- # A more secure key and IV generation is needed in the future
114
- # IV should be randomly generated and provided unencrypted
115
- # salt should be randomly generated and provided unencrypted (like in the current implementation)
116
- # key should be generated with OpenSSL::KDF::pbkdf2_hmac with properly chosen parameters
117
- # Short explanation about salt and IV: https://stackoverflow.com/a/1950674/6324550
118
112
  def encrypt_specific_file(path: nil, password: nil)
119
113
  UI.user_error!("No password supplied") if password.to_s.strip.length == 0
120
-
121
- data_to_encrypt = File.binread(path)
122
- salt = SecureRandom.random_bytes(8)
123
-
124
- # The :: is important, as there is a name clash
125
- cipher = ::OpenSSL::Cipher.new('AES-256-CBC')
126
- cipher.encrypt
127
- cipher.pkcs5_keyivgen(password, salt, 1, "MD5")
128
- encrypted_data = "Salted__" + salt + cipher.update(data_to_encrypt) + cipher.final
129
-
130
- File.write(path, Base64.encode64(encrypted_data))
114
+ e = MatchFileEncryption.new
115
+ e.encrypt(file_path: path, password: password)
131
116
  rescue FastlaneCore::Interface::FastlaneError
132
117
  raise
133
118
  rescue => error
@@ -135,28 +120,12 @@ module Match
135
120
  UI.crash!("Error encrypting '#{path}'")
136
121
  end
137
122
 
138
- # The encryption parameters in this implementations reflect the old behavior which depended on the users' local OpenSSL version
139
- # 1.0.x OpenSSL and earlier versions use MD5, 1.1.0c and newer uses SHA256, we try both before giving an error
140
- def decrypt_specific_file(path: nil, password: nil, hash_algorithm: "MD5")
141
- stored_data = Base64.decode64(File.read(path))
142
- salt = stored_data[8..15]
143
- data_to_decrypt = stored_data[16..-1]
144
-
145
- decipher = ::OpenSSL::Cipher.new('AES-256-CBC')
146
- decipher.decrypt
147
- decipher.pkcs5_keyivgen(password, salt, 1, hash_algorithm)
148
-
149
- decrypted_data = decipher.update(data_to_decrypt) + decipher.final
150
-
151
- File.binwrite(path, decrypted_data)
123
+ def decrypt_specific_file(path: nil, password: nil)
124
+ e = MatchFileEncryption.new
125
+ e.decrypt(file_path: path, password: password)
152
126
  rescue => error
153
- fallback_hash_algorithm = "SHA256"
154
- if hash_algorithm != fallback_hash_algorithm
155
- decrypt_specific_file(path: path, password: password, hash_algorithm: fallback_hash_algorithm)
156
- else
157
- UI.error(error.to_s)
158
- UI.crash!("Error decrypting '#{path}'")
159
- end
127
+ UI.error(error.to_s)
128
+ UI.crash!("Error decrypting '#{path}'")
160
129
  end
161
130
  end
162
131
  end
@@ -1,5 +1,6 @@
1
1
  require_relative 'encryption/interface'
2
2
  require_relative 'encryption/openssl'
3
+ require_relative 'encryption/encryption'
3
4
 
4
5
  module Match
5
6
  module Encryption
@@ -19,6 +19,7 @@ module Match
19
19
  # rubocop:disable Metrics/ClassLength
20
20
  class Runner
21
21
  attr_accessor :files_to_commit
22
+ attr_accessor :files_to_delete
22
23
  attr_accessor :spaceship
23
24
 
24
25
  attr_accessor :storage
@@ -28,6 +29,7 @@ module Match
28
29
  # rubocop:disable Metrics/PerceivedComplexity
29
30
  def run(params)
30
31
  self.files_to_commit = []
32
+ self.files_to_delete = []
31
33
 
32
34
  FileUtils.mkdir_p(params[:output_path]) if params[:output_path]
33
35
 
@@ -82,12 +84,12 @@ module Match
82
84
  end
83
85
 
84
86
  # Certificate
85
- cert_id = fetch_certificate(params: params, working_directory: storage.working_directory)
87
+ cert_id = fetch_certificate(params: params, renew_expired_certs: false)
86
88
 
87
89
  # Mac Installer Distribution Certificate
88
90
  additional_cert_types = params[:additional_cert_types] || []
89
91
  cert_ids = additional_cert_types.map do |additional_cert_type|
90
- fetch_certificate(params: params, working_directory: storage.working_directory, specific_cert_type: additional_cert_type)
92
+ fetch_certificate(params: params, renew_expired_certs: false, specific_cert_type: additional_cert_type)
91
93
  end
92
94
 
93
95
  profile_type = Sigh.profile_type_for_distribution_type(
@@ -112,9 +114,10 @@ module Match
112
114
  end
113
115
  end
114
116
 
115
- if self.files_to_commit.count > 0 && !params[:readonly]
117
+ has_file_changes = self.files_to_commit.count > 0 || self.files_to_delete.count > 0
118
+ if has_file_changes && !params[:readonly]
116
119
  encryption.encrypt_files if encryption
117
- storage.save_changes!(files_to_commit: self.files_to_commit)
120
+ storage.save_changes!(files_to_commit: self.files_to_commit, files_to_delete: self.files_to_delete)
118
121
  end
119
122
 
120
123
  # Print a summary table for each app_identifier
@@ -152,13 +155,48 @@ module Match
152
155
  end
153
156
  end
154
157
 
155
- def fetch_certificate(params: nil, working_directory: nil, specific_cert_type: nil)
158
+ RENEWABLE_CERT_TYPES_VIA_API = [:mac_installer_distribution, :development, :distribution, :enterprise]
159
+
160
+ def fetch_certificate(params: nil, renew_expired_certs: false, specific_cert_type: nil)
156
161
  cert_type = Match.cert_type_sym(specific_cert_type || params[:type])
157
162
 
158
163
  certs = Dir[File.join(prefixed_working_directory, "certs", cert_type.to_s, "*.cer")]
159
164
  keys = Dir[File.join(prefixed_working_directory, "certs", cert_type.to_s, "*.p12")]
160
165
 
161
- if certs.count == 0 || keys.count == 0
166
+ storage_has_certs = certs.count != 0 && keys.count != 0
167
+
168
+ # Determine if cert is renewable.
169
+ # Can't renew developer_id certs with Connect API token. Account holder access is required.
170
+ is_authenticated_with_login = Spaceship::ConnectAPI.token.nil?
171
+ is_cert_renewable_via_api = RENEWABLE_CERT_TYPES_VIA_API.include?(cert_type)
172
+ is_cert_renewable = is_authenticated_with_login || is_cert_renewable_via_api
173
+
174
+ # Validate existing certificate first.
175
+ if renew_expired_certs && is_cert_renewable && storage_has_certs && !params[:readonly]
176
+ cert_path = select_cert_or_key(paths: certs)
177
+
178
+ unless Utils.is_cert_valid?(cert_path)
179
+ UI.important("Removing invalid certificate '#{File.basename(cert_path)}'")
180
+
181
+ # Remove expired cert.
182
+ self.files_to_delete << cert_path
183
+ File.delete(cert_path)
184
+
185
+ # Key filename is the same as cert but with .p12 extension.
186
+ key_path = cert_path.gsub(/\.cer$/, ".p12")
187
+ # Remove expired key .p12 file.
188
+ if File.exist?(key_path)
189
+ self.files_to_delete << key_path
190
+ File.delete(key_path)
191
+ end
192
+
193
+ certs = []
194
+ keys = []
195
+ storage_has_certs = false
196
+ end
197
+ end
198
+
199
+ if !storage_has_certs
162
200
  UI.important("Couldn't find a valid code signing identity for #{cert_type}... creating one for you now")
163
201
  UI.crash!("No code signing identity found and cannot create a new one because you enabled `readonly`") if params[:readonly]
164
202
  cert_path = Generator.generate_certificate(params, cert_type, prefixed_working_directory, specific_cert_type: specific_cert_type)
@@ -140,9 +140,10 @@ module Match
140
140
  end
141
141
 
142
142
  def delete_files(files_to_delete: [], custom_message: nil)
143
- # No specific list given, e.g. this happens on `fastlane match nuke`
144
- # We just want to run `git add -A` to commit everything
145
- git_push(commands: ["git add -A"], commit_message: custom_message)
143
+ if files_to_delete.count > 0
144
+ commands = files_to_delete.map { |filename| "git rm #{filename.shellescape}" }
145
+ git_push(commands: commands, commit_message: custom_message)
146
+ end
146
147
  end
147
148
 
148
149
  def upload_files(files_to_upload: [], custom_message: nil)
@@ -55,11 +55,14 @@ module Match
55
55
  # Custom init to `[]` in case `nil` is passed
56
56
  files_to_commit ||= []
57
57
  files_to_delete ||= []
58
+ files_to_delete -= files_to_commit # Make sure we are not removing added files.
59
+
60
+ if files_to_commit.count == 0 && files_to_delete.count == 0
61
+ UI.user_error!("Neither `files_to_commit` nor `files_to_delete` were provided to the `save_changes!` method call")
62
+ end
58
63
 
59
64
  Dir.chdir(File.expand_path(self.working_directory)) do
60
65
  if files_to_commit.count > 0 # everything that isn't `match nuke`
61
- UI.user_error!("You can't provide both `files_to_delete` and `files_to_commit` right now") if files_to_delete.count > 0
62
-
63
66
  if !File.exist?(MATCH_VERSION_FILE_NAME) || File.read(MATCH_VERSION_FILE_NAME) != Fastlane::VERSION.to_s
64
67
  files_to_commit << MATCH_VERSION_FILE_NAME
65
68
  File.write(MATCH_VERSION_FILE_NAME, Fastlane::VERSION) # stored unencrypted
@@ -74,13 +77,14 @@ module Match
74
77
 
75
78
  self.upload_files(files_to_upload: files_to_commit, custom_message: custom_message)
76
79
  UI.message("Finished uploading files to #{self.human_readable_description}")
77
- elsif files_to_delete.count > 0
80
+ end
81
+
82
+ if files_to_delete.count > 0
78
83
  self.delete_files(files_to_delete: files_to_delete, custom_message: custom_message)
79
84
  UI.message("Finished deleting files from #{self.human_readable_description}")
80
- else
81
- UI.user_error!("Neither `files_to_commit` nor `files_to_delete` were provided to the `save_changes!` method call")
82
85
  end
83
86
  end
87
+ ensure # Always clear working_directory after save
84
88
  self.clear_changes
85
89
  end
86
90
 
@@ -65,6 +65,7 @@ module Pilot
65
65
  UI.important("`skip_waiting_for_build_processing` used and no `changelog` supplied - skipping waiting for build processing")
66
66
  return
67
67
  else
68
+ UI.important("`skip_waiting_for_build_processing` used and `changelog` supplied - will wait until build appears on App Store Connect, update the changelog and then skip the rest of the remaining of the processing steps.")
68
69
  return_when_build_appears = true
69
70
  end
70
71
  end
@@ -72,8 +73,10 @@ module Pilot
72
73
  # Calling login again here is needed if login was not called during 'start'
73
74
  login unless should_login_in_start
74
75
 
75
- UI.message("If you want to skip waiting for the processing to be finished, use the `skip_waiting_for_build_processing` option")
76
- UI.message("Note that if `skip_waiting_for_build_processing` is used but a `changelog` is supplied, this process will wait for the build to appear on AppStoreConnect, update the changelog and then skip the remaining of the processing steps.")
76
+ if config[:skip_waiting_for_build_processing].nil?
77
+ UI.message("If you want to skip waiting for the processing to be finished, use the `skip_waiting_for_build_processing` option")
78
+ UI.message("Note that if `skip_waiting_for_build_processing` is used but a `changelog` is supplied, this process will wait for the build to appear on App Store Connect, update the changelog and then skip the remaining of the processing steps.")
79
+ end
77
80
 
78
81
  latest_build = wait_for_build_processing_to_be_complete(return_when_build_appears)
79
82
  distribute(options, build: latest_build)
@@ -365,12 +368,17 @@ module Pilot
365
368
  end
366
369
 
367
370
  def reject_build_waiting_for_review(build)
368
- waiting_for_review_build = build.app.get_builds(filter: { "betaAppReviewSubmission.betaReviewState" => "WAITING_FOR_REVIEW" }, includes: "betaAppReviewSubmission,preReleaseVersion").first
371
+ waiting_for_review_build = build.app.get_builds(
372
+ filter: { "betaAppReviewSubmission.betaReviewState" => "WAITING_FOR_REVIEW,IN_REVIEW",
373
+ "expired" => false,
374
+ "preReleaseVersion.version" => build.pre_release_version.version },
375
+ includes: "betaAppReviewSubmission,preReleaseVersion"
376
+ ).first
369
377
  unless waiting_for_review_build.nil?
370
378
  UI.important("Another build is already in review. Going to remove that build and submit the new one.")
371
- UI.important("Deleting beta app review submission for build: #{waiting_for_review_build.app_version} - #{waiting_for_review_build.version}")
372
- waiting_for_review_build.beta_app_review_submission.delete!
373
- UI.success("Deleted beta app review submission for previous build: #{waiting_for_review_build.app_version} - #{waiting_for_review_build.version}")
379
+ UI.important("Canceling beta app review submission for build: #{waiting_for_review_build.app_version} - #{waiting_for_review_build.version}")
380
+ waiting_for_review_build.expire!
381
+ UI.success("Canceled beta app review submission for previous build: #{waiting_for_review_build.app_version} - #{waiting_for_review_build.version}")
374
382
  end
375
383
  end
376
384
 
@@ -85,8 +85,8 @@ module Pilot
85
85
  result ||= FastlaneCore::IpaFileAnalyser.fetch_app_platform(config[:ipa]) if config[:ipa]
86
86
  result ||= FastlaneCore::PkgFileAnalyser.fetch_app_platform(config[:pkg]) if config[:pkg]
87
87
  if required
88
- result ||= UI.input("Please enter the app's platform (appletvos, ios, osx): ")
89
- UI.user_error!("App Platform must be ios, appletvos, or osx") unless ['ios', 'appletvos', 'osx'].include?(result)
88
+ result ||= UI.input("Please enter the app's platform (appletvos, ios, osx, xros): ")
89
+ UI.user_error!("App Platform must be ios, appletvos, osx, or xros") unless ['ios', 'appletvos', 'osx', 'xros'].include?(result)
90
90
  UI.verbose("App Platform (#{result})")
91
91
  end
92
92
  return result
@@ -49,7 +49,7 @@ module Pilot
49
49
  description: "The platform to use (optional)",
50
50
  optional: true,
51
51
  verify_block: proc do |value|
52
- UI.user_error!("The platform can only be ios, appletvos, or osx") unless ['ios', 'appletvos', 'osx'].include?(value)
52
+ UI.user_error!("The platform can only be ios, appletvos, osx, or xros") unless ['ios', 'appletvos', 'osx', 'xros'].include?(value)
53
53
  end),
54
54
  FastlaneCore::ConfigItem.new(key: :apple_id,
55
55
  short_option: "-p",
@@ -23,7 +23,7 @@ module Snapshot
23
23
  short_option: "-w",
24
24
  env_name: "SNAPSHOT_WORKSPACE",
25
25
  optional: true,
26
- description: "Path the workspace file",
26
+ description: "Path to the workspace file",
27
27
  verify_block: proc do |value|
28
28
  v = File.expand_path(value.to_s)
29
29
  UI.user_error!("Workspace file not found at path '#{v}'") unless File.exist?(v)
@@ -34,7 +34,7 @@ module Snapshot
34
34
  short_option: "-p",
35
35
  optional: true,
36
36
  env_name: "SNAPSHOT_PROJECT",
37
- description: "Path the project file",
37
+ description: "Path to the project file",
38
38
  verify_block: proc do |value|
39
39
  v = File.expand_path(value.to_s)
40
40
  UI.user_error!("Project file not found at path '#{v}'") unless File.exist?(v)
@@ -34,7 +34,7 @@ module Snapshot
34
34
  puts("✅ Successfully created #{snapshot_helper_filename} '#{File.join(path, snapshot_helper_filename)}'".green)
35
35
  puts("✅ Successfully created new Snapfile at '#{snapfile_path}'".green)
36
36
  puts("-------------------------------------------------------".yellow)
37
- print_instructions(snapshot_helper_filename: snapshot_helper_filename, snapfile_path: snapfile_path)
37
+ print_instructions(snapshot_helper_filename: snapshot_helper_filename)
38
38
  end
39
39
 
40
40
  def self.print_instructions(snapshot_helper_filename: nil)
@@ -69,7 +69,7 @@ module Spaceship
69
69
  # Forwarding to class level if using web session.
70
70
  def hostname
71
71
  if @token
72
- return "https://api.appstoreconnect.apple.com/v1/"
72
+ return "https://api.appstoreconnect.apple.com/"
73
73
  end
74
74
  return self.class.hostname
75
75
  end
@@ -184,7 +184,7 @@ module Spaceship
184
184
  if tries.zero?
185
185
  raise error
186
186
  else
187
- msg = "Token has expired or has been revoked! Trying to refresh..."
187
+ msg = "Token has expired, issued-at-time is in the future, or has been revoked! Trying to refresh..."
188
188
  puts(msg) if Spaceship::Globals.verbose?
189
189
  @token.refresh!
190
190
  retry