fastlane 2.239.0 → 2.240.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +104 -104
  3. data/credentials_manager/lib/credentials_manager/account_manager.rb +6 -2
  4. data/deliver/lib/deliver/detect_values.rb +3 -1
  5. data/deliver/lib/deliver/runner.rb +5 -4
  6. data/fastlane/lib/fastlane/actions/build_and_upload_to_appetize.rb +3 -1
  7. data/fastlane/lib/fastlane/actions/lcov.rb +3 -1
  8. data/fastlane/lib/fastlane/actions/spaceship_logs.rb +4 -2
  9. data/fastlane/lib/fastlane/actions/swiftlint.rb +6 -3
  10. data/fastlane/lib/fastlane/actions/update_project_provisioning.rb +3 -1
  11. data/fastlane/lib/fastlane/version.rb +1 -1
  12. data/fastlane/swift/Deliverfile.swift +1 -1
  13. data/fastlane/swift/DeliverfileProtocol.swift +1 -1
  14. data/fastlane/swift/Fastlane.swift +38 -2
  15. data/fastlane/swift/Gymfile.swift +1 -1
  16. data/fastlane/swift/GymfileProtocol.swift +8 -1
  17. data/fastlane/swift/Matchfile.swift +1 -1
  18. data/fastlane/swift/MatchfileProtocol.swift +1 -1
  19. data/fastlane/swift/Precheckfile.swift +1 -1
  20. data/fastlane/swift/PrecheckfileProtocol.swift +1 -1
  21. data/fastlane/swift/Scanfile.swift +1 -1
  22. data/fastlane/swift/ScanfileProtocol.swift +8 -1
  23. data/fastlane/swift/Screengrabfile.swift +1 -1
  24. data/fastlane/swift/ScreengrabfileProtocol.swift +1 -1
  25. data/fastlane/swift/Snapshotfile.swift +1 -1
  26. data/fastlane/swift/SnapshotfileProtocol.swift +8 -1
  27. data/fastlane_core/lib/fastlane_core/configuration/configuration.rb +12 -0
  28. data/fastlane_core/lib/fastlane_core/fastlane_pty.rb +20 -7
  29. data/fastlane_core/lib/fastlane_core/ipa_file_analyser.rb +1 -1
  30. data/fastlane_core/lib/fastlane_core/itunes_transporter.rb +16 -13
  31. data/fastlane_core/lib/fastlane_core/keychain_importer.rb +7 -1
  32. data/fastlane_core/lib/fastlane_core/project.rb +19 -0
  33. data/gym/lib/gym/options.rb +5 -0
  34. data/match/lib/match/encryption/openssl.rb +6 -2
  35. data/match/lib/match/runner.rb +30 -3
  36. data/match/lib/match/storage/interface.rb +8 -5
  37. data/scan/lib/scan/options.rb +5 -0
  38. data/snapshot/lib/snapshot/options.rb +5 -0
  39. data/spaceship/lib/spaceship/client.rb +137 -15
  40. data/spaceship/lib/spaceship/du/upload_file.rb +4 -3
  41. data/trainer/lib/trainer/xcresult/helper.rb +27 -13
  42. metadata +43 -29
@@ -35,7 +35,7 @@ module FastlaneCore
35
35
 
36
36
  private_constant :ERROR_REGEX, :WARNING_REGEX, :OUTPUT_REGEX, :RETURN_VALUE_REGEX, :SKIP_ERRORS
37
37
 
38
- def build_download_command(username, password, apple_id, destination = "/tmp", provider_short_name = "", jwt = nil)
38
+ def build_download_command(username, password, apple_id, destination = Dir.tmpdir, provider_short_name = "", jwt = nil)
39
39
  not_implemented(__method__)
40
40
  end
41
41
 
@@ -43,11 +43,11 @@ module FastlaneCore
43
43
  not_implemented(__method__)
44
44
  end
45
45
 
46
- def build_upload_command(username, password, source = "/tmp", options = {})
46
+ def build_upload_command(username, password, source = Dir.tmpdir, options = {})
47
47
  not_implemented(__method__)
48
48
  end
49
49
 
50
- def build_verify_command(username, password, source = "/tmp", provider_short_name = "", **kwargs)
50
+ def build_verify_command(username, password, source = Dir.tmpdir, provider_short_name = "", **kwargs)
51
51
  not_implemented(__method__)
52
52
  end
53
53
 
@@ -291,6 +291,7 @@ module FastlaneCore
291
291
 
292
292
  @errors << "The call to the altool completed with a non-zero exit status: #{exit_status}. This indicates a failure." unless exit_status.zero?
293
293
  @errors << "-1 indicates altool exited abnormally; try retrying (see https://github.com/fastlane/fastlane/issues/21535)" if exit_status == -1
294
+ @errors.clear if @all_lines.any? { |line| line.include?("UPLOAD SUCCEEDED with no errors") }
294
295
 
295
296
  unless @errors.empty? || @all_lines.empty?
296
297
  @all_lines.each do |line|
@@ -319,7 +320,7 @@ module FastlaneCore
319
320
  end
320
321
  end
321
322
 
322
- def build_upload_command(username, password, source = "/tmp", options = {})
323
+ def build_upload_command(username, password, source = Dir.tmpdir, options = {})
323
324
  provider_short_name = options.fetch(:provider_short_name, "")
324
325
  provider_public_id = options.fetch(:provider_public_id, "")
325
326
  jwt = options[:jwt]
@@ -351,11 +352,11 @@ module FastlaneCore
351
352
  ].compact.join(' ')
352
353
  end
353
354
 
354
- def build_download_command(username, password, apple_id, destination = "/tmp", provider_short_name = "", jwt = nil)
355
+ def build_download_command(username, password, apple_id, destination = Dir.tmpdir, provider_short_name = "", jwt = nil)
355
356
  raise "This feature has not been implemented yet with altool for Xcode 14"
356
357
  end
357
358
 
358
- def build_verify_command(username, password, source = "/tmp", options = {})
359
+ def build_verify_command(username, password, source = Dir.tmpdir, options = {})
359
360
  provider_short_name = options.fetch(:provider_short_name, "")
360
361
  provider_public_id = options.fetch(:provider_public_id, "")
361
362
  api_key = options[:api_key]
@@ -442,7 +443,7 @@ module FastlaneCore
442
443
  end
443
444
  end
444
445
 
445
- def build_upload_command(username, password, source = "/tmp", options = {})
446
+ def build_upload_command(username, password, source = Dir.tmpdir, options = {})
446
447
  provider_short_name = options.fetch(:provider_short_name, "")
447
448
  jwt = options[:jwt]
448
449
  api_key = options[:api_key]
@@ -458,7 +459,7 @@ module FastlaneCore
458
459
  ].compact.join(' ')
459
460
  end
460
461
 
461
- def build_download_command(username, password, apple_id, destination = "/tmp", provider_short_name = "", jwt = nil)
462
+ def build_download_command(username, password, apple_id, destination = Dir.tmpdir, provider_short_name = "", jwt = nil)
462
463
  [
463
464
  '"' + Helper.transporter_path + '"',
464
465
  "-m lookupMetadata",
@@ -477,7 +478,7 @@ module FastlaneCore
477
478
  ].compact.join(' ')
478
479
  end
479
480
 
480
- def build_verify_command(username, password, source = "/tmp", options = {})
481
+ def build_verify_command(username, password, source = Dir.tmpdir, options = {})
481
482
  provider_short_name = options.fetch(:provider_short_name, "")
482
483
  jwt = options[:jwt]
483
484
  [
@@ -568,7 +569,7 @@ module FastlaneCore
568
569
  end
569
570
  end
570
571
 
571
- def build_upload_command(username, password, source = "/tmp", options = {})
572
+ def build_upload_command(username, password, source = Dir.tmpdir, options = {})
572
573
  provider_short_name = options.fetch(:provider_short_name, "")
573
574
  jwt = options[:jwt]
574
575
  api_key = options[:api_key]
@@ -607,7 +608,7 @@ module FastlaneCore
607
608
  end
608
609
  end
609
610
 
610
- def build_verify_command(username, password, source = "/tmp", options = {})
611
+ def build_verify_command(username, password, source = Dir.tmpdir, options = {})
611
612
  provider_short_name = options.fetch(:provider_short_name, "")
612
613
  jwt = options[:jwt]
613
614
  credential_params = build_credential_params(username, password, jwt, nil, is_default_itms_on_xcode_11?)
@@ -641,7 +642,7 @@ module FastlaneCore
641
642
  end
642
643
  end
643
644
 
644
- def build_download_command(username, password, apple_id, destination = "/tmp", provider_short_name = "", jwt = nil)
645
+ def build_download_command(username, password, apple_id, destination = Dir.tmpdir, provider_short_name = "", jwt = nil)
645
646
  credential_params = build_credential_params(username, password, jwt, nil, is_default_itms_on_xcode_11?)
646
647
  if is_default_itms_on_xcode_11?
647
648
  [
@@ -793,7 +794,9 @@ module FastlaneCore
793
794
  # @raise [Deliver::TransporterTransferError] when something went wrong
794
795
  # when transferring
795
796
  def download(app_id, dir = nil)
796
- dir ||= "/tmp"
797
+ # Dir.tmpdir, not a literal "/tmp": the transporter chdirs into this
798
+ # directory, and "/tmp" is not one on Windows. See fastlane#30184.
799
+ dir ||= Dir.tmpdir
797
800
 
798
801
  password_placeholder = @jwt.nil? ? 'YourPassword' : nil
799
802
  jwt_placeholder = @jwt.nil? ? nil : 'YourJWT'
@@ -98,7 +98,13 @@ module FastlaneCore
98
98
  server = server_name(keychain_name)
99
99
 
100
100
  # Attempt to find password in keychain for keychain
101
- item = Security::InternetPassword.find(server: server)
101
+ begin
102
+ item = Security::InternetPassword.find(server: server)
103
+ rescue Security::Error => ex
104
+ UI.important("Could not read the keychain item #{server}: #{ex.message}")
105
+ item = nil
106
+ end
107
+
102
108
  if item
103
109
  keychain_password = item.password
104
110
  UI.important("Using keychain password from keychain item #{server} in #{keychain_path}")
@@ -412,6 +412,16 @@ module FastlaneCore
412
412
  # @param [String] The key of which we want the value for (e.g. "PRODUCT_NAME")
413
413
  def build_settings(key: nil, optional: true)
414
414
  unless @build_settings
415
+ if (disallowed_by = xcodebuild_settings_lookup_disallowed_by)
416
+ message = "Could not read the '#{key}' build setting: fetching build settings by running" \
417
+ " `xcodebuild -showBuildSettings` is disallowed by #{disallowed_by}."
418
+ trigger = caller.find { |frame| !frame.start_with?(__FILE__) }
419
+ message += "\nThe build setting lookup was triggered by: #{trigger}" if trigger
420
+ message += "\nTo fix this, manually specify the option whose automatic detection required the '#{key}' build setting," \
421
+ " or disable #{disallowed_by} to allow fastlane to fetch it automatically."
422
+ UI.user_error!(message)
423
+ end
424
+
415
425
  if is_workspace
416
426
  if schemes.count == 0
417
427
  UI.user_error!("Could not find any schemes for Xcode workspace at path '#{self.path}'. Please make sure that the schemes you want to use are marked as `Shared` from Xcode.")
@@ -563,5 +573,14 @@ module FastlaneCore
563
573
  def automated_scheme_selection?
564
574
  FastlaneCore::Env.truthy?("AUTOMATED_SCHEME_SELECTION")
565
575
  end
576
+
577
+ # Returns a description of what disallowed fetching build settings via the
578
+ # (potentially slow on large projects) `xcodebuild -showBuildSettings` command,
579
+ # or nil if the lookup is allowed
580
+ def xcodebuild_settings_lookup_disallowed_by
581
+ return "the FASTLANE_DISALLOW_XCODEBUILD_SETTINGS_LOOKUP environment variable" if FastlaneCore::Env.truthy?("FASTLANE_DISALLOW_XCODEBUILD_SETTINGS_LOOKUP")
582
+ return "the `disallow_xcodebuild_settings_lookup` option" if options[:disallow_xcodebuild_settings_lookup]
583
+ nil
584
+ end
566
585
  end
567
586
  end
@@ -321,6 +321,11 @@ module Gym
321
321
  description: "Skips resolution of Swift Package Manager dependencies",
322
322
  type: Boolean,
323
323
  default_value: false),
324
+ FastlaneCore::ConfigItem.new(key: :disallow_xcodebuild_settings_lookup,
325
+ env_name: "GYM_DISALLOW_XCODEBUILD_SETTINGS_LOOKUP",
326
+ description: "Raises an error instead of fetching build settings by running `xcodebuild -showBuildSettings`, which can take a long time on large projects. The error names the required build setting, so the corresponding option can be specified manually",
327
+ type: Boolean,
328
+ default_value: false),
324
329
  FastlaneCore::ConfigItem.new(key: :disable_package_automatic_updates,
325
330
  env_name: "GYM_DISABLE_PACKAGE_AUTOMATIC_UPDATES",
326
331
  description: "Prevents packages from automatically being resolved to versions other than those recorded in the `Package.resolved` file. This translates in the option `-disableAutomaticPackageResolution` being passed to xcodebuild",
@@ -93,8 +93,12 @@ module Match
93
93
  def fetch_password!
94
94
  password = ENV["MATCH_PASSWORD"]
95
95
  unless password
96
- item = Security::InternetPassword.find(server: server_name(self.keychain_name))
97
- password = item.password if item
96
+ begin
97
+ item = Security::InternetPassword.find(server: server_name(self.keychain_name))
98
+ password = item.password if item
99
+ rescue Security::Error => ex
100
+ UI.important("Could not read the local keychain: #{ex.message}")
101
+ end
98
102
  end
99
103
 
100
104
  unless password
@@ -24,6 +24,8 @@ module Match
24
24
 
25
25
  attr_accessor :storage
26
26
 
27
+ attr_accessor :encryption
28
+
27
29
  attr_accessor :cache
28
30
 
29
31
  # rubocop:disable Metrics/PerceivedComplexity
@@ -45,14 +47,14 @@ module Match
45
47
  storage.download
46
48
 
47
49
  # Init the encryption only after the `storage.download` was called to have the right working directory
48
- encryption = Encryption.for_storage_mode(params[:storage_mode], {
50
+ self.encryption = Encryption.for_storage_mode(params[:storage_mode], {
49
51
  git_url: params[:git_url],
50
52
  s3_bucket: params[:s3_bucket],
51
53
  s3_skip_encryption: params[:s3_skip_encryption],
52
54
  working_directory: storage.working_directory,
53
55
  force_legacy_encryption: params[:force_legacy_encryption]
54
56
  })
55
- encryption.decrypt_files if encryption
57
+ self.encryption.decrypt_files if self.encryption
56
58
 
57
59
  unless params[:readonly]
58
60
  self.spaceship = SpaceshipEnsure.new(params[:username], params[:team_id], params[:team_name], api_token(params))
@@ -117,7 +119,7 @@ module Match
117
119
 
118
120
  has_file_changes = self.files_to_commit.count > 0 || self.files_to_delete.count > 0
119
121
  if has_file_changes && !params[:readonly]
120
- encryption.encrypt_files if encryption
122
+ self.encryption.encrypt_files if self.encryption
121
123
  storage.save_changes!(files_to_commit: self.files_to_commit, files_to_delete: self.files_to_delete)
122
124
  end
123
125
 
@@ -209,6 +211,10 @@ module Match
209
211
  self.files_to_commit << cert_path
210
212
  self.files_to_commit << private_key_path
211
213
 
214
+ # We pause to upload certs immediatly (or renewals) in case later logic fails, leaving
215
+ # AppStore with a set of files de-synced from match storage. See fastlane/fastlane#30140
216
+ save_changes_immediately!(params)
217
+
212
218
  # Reset certificates cache since we have a new cert.
213
219
  self.cache.reset_certificates
214
220
  else
@@ -255,6 +261,27 @@ module Match
255
261
  return File.basename(cert_path).gsub(".cer", "") # Certificate ID
256
262
  end
257
263
 
264
+ # Saves all pending file changes to storage right away, keeping the
265
+ # working directory around so the current run can continue using it
266
+ def save_changes_immediately!(params)
267
+ return if params[:readonly]
268
+ return if self.files_to_commit.empty? && self.files_to_delete.empty?
269
+
270
+ self.encryption.encrypt_files if self.encryption
271
+ begin
272
+ self.storage.save_changes!(files_to_commit: self.files_to_commit,
273
+ files_to_delete: self.files_to_delete,
274
+ clear_working_directory: false)
275
+ ensure
276
+ # Always hand back a decrypted working directory, 2nd upload will hopefully work.
277
+ self.encryption.decrypt_files if self.encryption
278
+ end
279
+
280
+ # We may have another upload at end (profiles) so reset our cert file(s).
281
+ self.files_to_commit = []
282
+ self.files_to_delete = []
283
+ end
284
+
258
285
  # @return [String] Path to certificate or P12 key
259
286
  def select_cert_or_key(paths:, certificate_id: nil)
260
287
  return paths.last if certificate_id.to_s.strip.empty?
@@ -47,11 +47,14 @@ module Match
47
47
  # given remote server
48
48
  # This method is blocking, meaning it might take multiple
49
49
  # seconds or longer to run
50
- # @parameter files_to_commit [Array] Array to paths to files
50
+ # @param files_to_commit [Array] Array to paths to files
51
51
  # that should be committed to the storage provider
52
- # @parameter custom_message: [String] Custom change message
53
- # that's optional, is used for commit title
54
- def save_changes!(files_to_commit: nil, files_to_delete: nil, custom_message: nil)
52
+ # @param custom_message: [String] Custom change message
53
+ # that's optional, is used for commit title
54
+ # @param clear_working_directory: [Bool] Pass `false` to keep
55
+ # the local working directory after saving, so more
56
+ # changes can be made and saved later in the same run
57
+ def save_changes!(files_to_commit: nil, files_to_delete: nil, custom_message: nil, clear_working_directory: true)
55
58
  # Custom init to `[]` in case `nil` is passed
56
59
  files_to_commit ||= []
57
60
  files_to_delete ||= []
@@ -85,7 +88,7 @@ module Match
85
88
  end
86
89
  end
87
90
  ensure # Always clear working_directory after save
88
- self.clear_changes
91
+ self.clear_changes if clear_working_directory
89
92
  end
90
93
 
91
94
  def upload_files(files_to_upload: [], custom_message: nil)
@@ -527,6 +527,11 @@ module Scan
527
527
  description: "Skips resolution of Swift Package Manager dependencies",
528
528
  type: Boolean,
529
529
  default_value: false),
530
+ FastlaneCore::ConfigItem.new(key: :disallow_xcodebuild_settings_lookup,
531
+ env_name: "SCAN_DISALLOW_XCODEBUILD_SETTINGS_LOOKUP",
532
+ description: "Raises an error instead of fetching build settings by running `xcodebuild -showBuildSettings`, which can take a long time on large projects. The error names the required build setting, so the corresponding option can be specified manually",
533
+ type: Boolean,
534
+ default_value: false),
530
535
  FastlaneCore::ConfigItem.new(key: :disable_package_automatic_updates,
531
536
  env_name: "SCAN_DISABLE_PACKAGE_AUTOMATIC_UPDATES",
532
537
  description: "Prevents packages from automatically being resolved to versions other than those recorded in the `Package.resolved` file. This translates in the option `-disableAutomaticPackageResolution` being passed to xcodebuild",
@@ -268,6 +268,11 @@ module Snapshot
268
268
  description: "Skips resolution of Swift Package Manager dependencies",
269
269
  type: Boolean,
270
270
  default_value: false),
271
+ FastlaneCore::ConfigItem.new(key: :disallow_xcodebuild_settings_lookup,
272
+ env_name: "SNAPSHOT_DISALLOW_XCODEBUILD_SETTINGS_LOOKUP",
273
+ description: "Raises an error instead of fetching build settings by running `xcodebuild -showBuildSettings`, which can take a long time on large projects. The error names the required build setting, so the corresponding option can be specified manually",
274
+ type: Boolean,
275
+ default_value: false),
271
276
  FastlaneCore::ConfigItem.new(key: :disable_package_automatic_updates,
272
277
  env_name: "SNAPSHOT_DISABLE_PACKAGE_AUTOMATIC_UPDATES",
273
278
  description: "Prevents packages from automatically being resolved to versions other than those recorded in the `Package.resolved` file. This translates in the option `-disableAutomaticPackageResolution` being passed to xcodebuild",
@@ -37,7 +37,7 @@ module Spaceship
37
37
  attr_accessor :user_email
38
38
 
39
39
  # The logger in which all requests are logged
40
- # /tmp/spaceship[time]_[pid].log by default
40
+ # <tmpdir>/spaceship[time]_[pid].log by default
41
41
  attr_accessor :logger
42
42
 
43
43
  attr_accessor :csrf_tokens
@@ -239,14 +239,19 @@ module Spaceship
239
239
  #####################################################
240
240
 
241
241
  # The logger in which all requests are logged
242
- # /tmp/spaceship[time]_[pid]_["threadid"].log by default
242
+ # <tmpdir>/spaceship[time]_[pid]_["threadid"].log by default
243
+ #
244
+ # Dir.tmpdir rather than a literal "/tmp", and it should stay that way:
245
+ # "/tmp" is not a directory on every platform Ruby runs on. On Windows it
246
+ # resolves against the current drive, so Logger.new raises Errno::ENOENT
247
+ # unless something else happens to have created it first.
243
248
  def logger
244
249
  unless @logger
245
250
  if ENV["VERBOSE"]
246
251
  @logger = Logger.new(STDOUT)
247
252
  else
248
253
  # Log to file by default
249
- path = "/tmp/spaceship#{Time.now.to_i}_#{Process.pid}_#{Thread.current.object_id}.log"
254
+ path = File.join(Dir.tmpdir, "spaceship#{Time.now.to_i}_#{Process.pid}_#{Thread.current.object_id}.log")
250
255
  @logger = Logger.new(path)
251
256
  end
252
257
 
@@ -716,28 +721,145 @@ module Spaceship
716
721
  exit(has_valid_session)
717
722
  end
718
723
 
724
+ # <tmpdir>/spaceship_itc_service_key.txt
725
+ #
726
+ # Dir.tmpdir rather than a literal "/tmp", for the same reason as #logger,
727
+ # and here the failure was worse than a missing file: itc_service_key
728
+ # rescues everything, so the write failing on Windows was reported as an
729
+ # App Store Connect outage. See #30198.
730
+ #
731
+ # On macOS this is also per user, so a cache written by one user no longer
732
+ # blocks another.
733
+ def itc_service_key_path
734
+ File.join(Dir.tmpdir, "spaceship_itc_service_key.txt")
735
+ end
736
+
719
737
  def itc_service_key
720
738
  return @service_key if @service_key
721
739
 
722
- # Check if we have a local cache of the key
723
- itc_service_key_path = "/tmp/spaceship_itc_service_key.txt"
724
- return File.read(itc_service_key_path) if File.exist?(itc_service_key_path)
740
+ # Check if we have a local cache of the key. Reading it is best effort:
741
+ # an unreadable cache is a reason to fetch the key again, not to fail.
742
+ begin
743
+ return File.read(itc_service_key_path) if File.exist?(itc_service_key_path)
744
+ rescue SystemCallError => ex
745
+ logger.warn("Could not read the cached App Store Connect API key: #{ex.message}")
746
+ end
747
+
748
+ @service_key = fetch_service_key
749
+
750
+ # Cache the key locally. Best effort as well: having the key and failing
751
+ # to save it is not a reason to fail the login. See fastlane#30198.
752
+ begin
753
+ File.write(itc_service_key_path, @service_key)
754
+ rescue SystemCallError => ex
755
+ logger.warn("Could not cache the App Store Connect API key at #{itc_service_key_path}: #{ex.message}")
756
+ end
757
+
758
+ return @service_key
759
+ end
760
+
761
+ # Two sources, because the one this used to rely on is gone.
762
+ #
763
+ # App Store Connect's sign out route answers with a redirect that carries
764
+ # the key its own front end is using:
765
+ #
766
+ # GET /logout -> 302 Location: .../appleauth/signout?widgetKey=<key>&...
767
+ #
768
+ # That is preferred over the olympus endpoint below, which Apple removed in
769
+ # September 2026 and which now answers 404. See fastlane#30199. The olympus
770
+ # call is kept as a fallback in case it comes back, since it is the source
771
+ # Apple documented rather than one scraped out of a redirect.
772
+ def fetch_service_key
773
+ key = fetch_service_key_from_signout
774
+ return key if key
725
775
 
726
776
  # Fixes issue https://github.com/fastlane/fastlane/issues/13281
727
777
  # Even though we are using https://appstoreconnect.apple.com, the service key needs to still use a
728
778
  # hostname through itunesconnect.apple.com
729
- response = request(:get, "https://appstoreconnect.apple.com/olympus/v1/app/config?hostname=itunesconnect.apple.com")
730
- @service_key = response.body["authServiceKey"].to_s
779
+ response = begin
780
+ request(:get, "https://appstoreconnect.apple.com/olympus/v1/app/config?hostname=itunesconnect.apple.com")
781
+ rescue Faraday::TimeoutError, Faraday::ConnectionFailed => ex
782
+ # The only case the old message was ever right about.
783
+ raise AppleTimeoutError.new, "Could not reach App Store Connect to fetch the API key: #{ex.message}"
784
+ end
731
785
 
732
- raise "Service key is empty" if @service_key.length == 0
786
+ extract_service_key(response)
787
+ end
733
788
 
734
- # Cache the key locally
735
- File.write(itc_service_key_path, @service_key)
789
+ # Read the key out of the sign out redirect, or nil if it is not there.
790
+ #
791
+ # Two things about this request matter, and neither is decoration.
792
+ #
793
+ # It sends no cookies. The redirect it returns points at a signout with
794
+ # `asop=destroy-session`, so asking for it over the client's own connection,
795
+ # which carries the session jar, would end the session this method is being
796
+ # called in order to establish. A bare Faraday connection has neither a
797
+ # cookie jar nor redirect following, which is exactly what is wanted here.
798
+ #
799
+ # It does not follow the redirect. The key is in the Location header;
800
+ # following it is what performs the signout.
801
+ #
802
+ # Failure returns nil rather than raising, so the caller falls through to
803
+ # the other source rather than this becoming a new single point of failure.
804
+ # Deliberately not `self.class.client` or anything built from it. A bare
805
+ # Faraday connection has no cookie jar and no redirect following, which is
806
+ # the whole point; the spec asserts both rather than trusting this comment.
807
+ def signout_connection
808
+ Faraday.new(url: "https://appstoreconnect.apple.com")
809
+ end
736
810
 
737
- return @service_key
738
- rescue => ex
739
- puts(ex.to_s)
740
- raise AppleTimeoutError.new, "Could not receive latest API key from App Store Connect, this might be a server issue."
811
+ def fetch_service_key_from_signout
812
+ response = signout_connection.head("/logout")
813
+
814
+ location = response.headers["location"].to_s
815
+ return nil if location.empty?
816
+
817
+ query = URI.parse(location).query
818
+ return nil if query.nil?
819
+
820
+ key = CGI.parse(query)["widgetKey"].first.to_s
821
+ return nil if key.empty?
822
+
823
+ logger.debug("Read the App Store Connect API key from the sign out redirect")
824
+ key
825
+ rescue Faraday::Error, URI::InvalidURIError => ex
826
+ # Only what this request can be expected to go wrong with. Rescuing
827
+ # everything here would put back the problem fastlane#30198 is about: a
828
+ # local failure, a missing log directory being the one that started it,
829
+ # would be swallowed and the caller would be handed whatever the fallback
830
+ # said instead of the real cause.
831
+ #
832
+ # Warn rather than debug. This is the source the key normally comes from,
833
+ # so failing here means the run is about to depend on an endpoint Apple has
834
+ # already removed once. If the fallback fails too, its message is all the
835
+ # caller sees, and this line is the half that says why.
836
+ logger.warn("Could not read the App Store Connect API key from the sign out redirect, falling back to the olympus endpoint: #{ex.message}")
837
+ nil
838
+ end
839
+
840
+ # The status matters, and used to be thrown away. A non 2xx response body is
841
+ # an HTML error page rather than JSON, and String#[] on it returns nil for
842
+ # the key being looked up, so the failure presented as an empty key no
843
+ # matter what had actually gone wrong. See fastlane#30199.
844
+ def extract_service_key(response)
845
+ status = response.status.to_i
846
+ body = response.body
847
+
848
+ unless (200..299).cover?(status)
849
+ detail = body.to_s.strip[0, 200]
850
+ message = "App Store Connect returned #{status} for the API key endpoint."
851
+ message += " #{detail}" unless detail.empty?
852
+
853
+ # 5xx and 429 are worth retrying, a 4xx is not, and with_retry decides
854
+ # that from the class rather than the message.
855
+ raise AppleTimeoutError.new, "#{message} This might be a temporary server error, check https://developer.apple.com/system-status/" if status >= 500
856
+ raise UnexpectedResponse.new, message
857
+ end
858
+
859
+ key = body.kind_of?(Hash) ? body["authServiceKey"].to_s : ""
860
+ raise UnexpectedResponse.new, "App Store Connect returned #{status} for the API key endpoint but no authServiceKey in the response." if key.empty?
861
+
862
+ key
741
863
  end
742
864
 
743
865
  #####################################################
@@ -1,3 +1,4 @@
1
+ require 'tmpdir'
1
2
  require 'fileutils'
2
3
 
3
4
  require_relative 'utilities'
@@ -30,10 +31,10 @@ module Spaceship
30
31
  end
31
32
 
32
33
  # As things like screenshots and app icon shouldn't contain the alpha channel
33
- # This will copy the image into /tmp to remove the alpha channel there
34
- # That's done to not edit the original image
34
+ # This will copy the image into the temporary directory to remove the
35
+ # alpha channel there. That's done to not edit the original image.
35
36
  def remove_alpha_channel(original)
36
- path = "/tmp/#{Digest::MD5.hexdigest(original)}.png"
37
+ path = File.join(Dir.tmpdir, "#{Digest::MD5.hexdigest(original)}.png")
37
38
  FileUtils.copy(original, path)
38
39
  if mac? # sips is only available on macOS
39
40
  `sips -s format bmp '#{path}' &> /dev/null` # &> /dev/null since there is warning because of the extension
@@ -32,6 +32,29 @@ module Trainer
32
32
  node['children'].select { |child| node_types.include?(child['nodeType']) }
33
33
  end
34
34
 
35
+ # The version of the xcresulttool on the current PATH
36
+ #
37
+ # Not memoized: `xcrun` resolves through DEVELOPER_DIR, which the xcode_select,
38
+ # xcversion, xcodes and xcode_install actions change during a run, so the answer
39
+ # can differ between two calls in the same process.
40
+ #
41
+ # @return [Gem::Version, nil] The version, or nil when there is no xcrun to ask
42
+ def self.xcresulttool_version
43
+ # e.g. DEVELOPER_DIR=/Applications/Xcode_16_beta_3.app
44
+ # xcresulttool version 23021, format version 3.53 (current)
45
+ # xcresulttool version 24408, schema version: 0.1.0 (legacy commands format version: 3.56)
46
+ output = `xcrun xcresulttool version`
47
+ match = output.match(/xcresulttool version (?<version>[\d.]+)/)
48
+ raise "Could not read the xcresulttool version from #{output.strip.inspect}" if match.nil?
49
+
50
+ Gem::Version.new(match[:version])
51
+ rescue Errno::ENOENT
52
+ # No xcrun at all: not macOS, or no developer tools installed. Every other
53
+ # failure is left to surface, so a change in what xcresulttool reports is not
54
+ # silently read as "this version is not supported".
55
+ nil
56
+ end
57
+
35
58
  # Check if the current xcresulttool supports new commands introduced in Xcode 16+
36
59
  #
37
60
  # Since Xcode 16b3, xcresulttool has marked `get <object> --format json` as deprecated/legacy,
@@ -39,24 +62,15 @@ module Trainer
39
62
  #
40
63
  # @return [Boolean] Whether the xcresulttool supports Xcode 16+ commands
41
64
  def self.supports_xcresulttool_version_23?
42
- # e.g. DEVELOPER_DIR=/Applications/Xcode_16_beta_3.app
43
- # xcresulttool version 23021, format version 3.53 (current)
44
- match = `xcrun xcresulttool version`.match(/xcresulttool version (?<version>[\d.]+)/)
45
- version = match[:version]
65
+ version = xcresulttool_version
46
66
 
47
- Gem::Version.new(version) >= Gem::Version.new(23_021)
48
- rescue
49
- false
67
+ !version.nil? && version >= Gem::Version.new(23_021)
50
68
  end
51
69
 
52
70
  def self.supports_xcresulttool_version_24?
53
- # xcresulttool version 24408, schema version: 0.1.0 (legacy commands format version: 3.56)
54
- match = `xcrun xcresulttool version`.match(/xcresulttool version (?<version>[\d.]+)/)
55
- version = match[:version]
71
+ version = xcresulttool_version
56
72
 
57
- Gem::Version.new(version) >= Gem::Version.new(24_408)
58
- rescue
59
- false
73
+ !version.nil? && version >= Gem::Version.new(24_408)
60
74
  end
61
75
  end
62
76
  end