fastlane 2.240.1 → 2.241.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 (157) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +51 -171
  3. data/bin/fastlane +4 -2
  4. data/credentials_manager/README.md +1 -1
  5. data/deliver/lib/deliver/app_screenshot.rb +8 -4
  6. data/deliver/lib/deliver/commands_generator.rb +1 -1
  7. data/deliver/lib/deliver/download_screenshots.rb +8 -1
  8. data/deliver/lib/deliver/sync_screenshots.rb +27 -28
  9. data/deliver/lib/deliver/upload_app_clip_default_experience_header_images.rb +3 -3
  10. data/deliver/lib/deliver/upload_metadata.rb +12 -16
  11. data/deliver/lib/deliver/upload_screenshots.rb +12 -14
  12. data/docs/README.md +25 -0
  13. data/fastlane/lib/assets/TeamTable.md.erb +28 -0
  14. data/fastlane/lib/fastlane/actions/appaloosa.rb +2 -1
  15. data/fastlane/lib/fastlane/actions/appium.rb +1 -1
  16. data/fastlane/lib/fastlane/actions/apteligent.rb +8 -1
  17. data/fastlane/lib/fastlane/actions/build_and_upload_to_appetize.rb +1 -1
  18. data/fastlane/lib/fastlane/actions/clean_build_artifacts.rb +3 -0
  19. data/fastlane/lib/fastlane/actions/danger.rb +5 -5
  20. data/fastlane/lib/fastlane/actions/docs/build_app.md +1 -2
  21. data/fastlane/lib/fastlane/actions/docs/create_app_online.md +1 -2
  22. data/fastlane/lib/fastlane/actions/docs/get_certificates.md +0 -2
  23. data/fastlane/lib/fastlane/actions/docs/get_push_certificate.md +1 -3
  24. data/fastlane/lib/fastlane/actions/docs/sync_code_signing.md +13 -3
  25. data/fastlane/lib/fastlane/actions/ensure_no_debug_code.rb +2 -2
  26. data/fastlane/lib/fastlane/actions/get_managed_play_store_publishing_rights.rb +1 -0
  27. data/fastlane/lib/fastlane/actions/github_api.rb +1 -2
  28. data/fastlane/lib/fastlane/actions/hg_commit_version_bump.rb +1 -1
  29. data/fastlane/lib/fastlane/actions/increment_build_number.rb +1 -1
  30. data/fastlane/lib/fastlane/actions/increment_version_number.rb +1 -1
  31. data/fastlane/lib/fastlane/actions/install_xcode_plugin.rb +16 -6
  32. data/fastlane/lib/fastlane/actions/lcov.rb +9 -2
  33. data/fastlane/lib/fastlane/actions/nexus_upload.rb +16 -4
  34. data/fastlane/lib/fastlane/actions/notification.rb +3 -2
  35. data/fastlane/lib/fastlane/actions/oclint.rb +8 -8
  36. data/fastlane/lib/fastlane/actions/push_to_git_remote.rb +9 -4
  37. data/fastlane/lib/fastlane/actions/register_device.rb +1 -1
  38. data/fastlane/lib/fastlane/actions/say.rb +2 -4
  39. data/fastlane/lib/fastlane/actions/set_pod_key.rb +3 -3
  40. data/fastlane/lib/fastlane/actions/sonar.rb +16 -16
  41. data/fastlane/lib/fastlane/actions/sourcedocs.rb +8 -8
  42. data/fastlane/lib/fastlane/actions/splunkmint.rb +8 -1
  43. data/fastlane/lib/fastlane/actions/update_project_provisioning.rb +1 -1
  44. data/fastlane/lib/fastlane/actions/upload_to_app_store.rb +5 -2
  45. data/fastlane/lib/fastlane/actions/upload_to_testflight.rb +5 -2
  46. data/fastlane/lib/fastlane/actions/verify_build.rb +1 -4
  47. data/fastlane/lib/fastlane/actions/verify_pod_keys.rb +3 -1
  48. data/fastlane/lib/fastlane/actions/xcode_server_get_assets.rb +20 -19
  49. data/fastlane/lib/fastlane/actions/xcodebuild.rb +5 -6
  50. data/fastlane/lib/fastlane/actions/zip.rb +2 -1
  51. data/fastlane/lib/fastlane/auto_complete.rb +1 -1
  52. data/fastlane/lib/fastlane/cli_tools_distributor.rb +14 -0
  53. data/fastlane/lib/fastlane/documentation/markdown_docs_generator.rb +23 -0
  54. data/fastlane/lib/fastlane/environment_printer.rb +12 -25
  55. data/fastlane/lib/fastlane/fast_file.rb +2 -1
  56. data/fastlane/lib/fastlane/fastlane_require.rb +5 -1
  57. data/fastlane/lib/fastlane/helper/sh_helper.rb +16 -6
  58. data/fastlane/lib/fastlane/new_action.rb +1 -1
  59. data/fastlane/lib/fastlane/plugins/plugin_fetcher.rb +1 -1
  60. data/fastlane/lib/fastlane/plugins/plugin_info_collector.rb +1 -1
  61. data/fastlane/lib/fastlane/plugins/plugin_manager.rb +36 -4
  62. data/fastlane/lib/fastlane/plugins/template/%gem_name%.gemspec.erb +1 -1
  63. data/fastlane/lib/fastlane/plugins/template/.github/workflows/test.yml +1 -1
  64. data/fastlane/lib/fastlane/plugins/template/.rubocop.yml +3 -7
  65. data/fastlane/lib/fastlane/swift_lane_manager.rb +22 -4
  66. data/fastlane/lib/fastlane/version.rb +3 -2
  67. data/fastlane/swift/Deliverfile.swift +1 -1
  68. data/fastlane/swift/DeliverfileProtocol.swift +1 -1
  69. data/fastlane/swift/Fastlane.swift +39 -9
  70. data/fastlane/swift/Gymfile.swift +1 -1
  71. data/fastlane/swift/GymfileProtocol.swift +1 -1
  72. data/fastlane/swift/MainProcess.swift +190 -20
  73. data/fastlane/swift/Matchfile.swift +1 -1
  74. data/fastlane/swift/MatchfileProtocol.swift +10 -3
  75. data/fastlane/swift/Precheckfile.swift +1 -1
  76. data/fastlane/swift/PrecheckfileProtocol.swift +1 -1
  77. data/fastlane/swift/Scanfile.swift +1 -1
  78. data/fastlane/swift/ScanfileProtocol.swift +8 -1
  79. data/fastlane/swift/Screengrabfile.swift +1 -1
  80. data/fastlane/swift/ScreengrabfileProtocol.swift +1 -1
  81. data/fastlane/swift/Snapshotfile.swift +1 -1
  82. data/fastlane/swift/SnapshotfileProtocol.swift +1 -1
  83. data/fastlane/swift/formatting/Rakefile +1 -0
  84. data/fastlane/swift/main.swift +4 -1
  85. data/fastlane_core/README.md +1 -1
  86. data/fastlane_core/lib/fastlane_core/analytics/analytics_event_builder.rb +1 -17
  87. data/fastlane_core/lib/fastlane_core/configuration/configuration.rb +11 -8
  88. data/fastlane_core/lib/fastlane_core/device_manager.rb +2 -2
  89. data/fastlane_core/lib/fastlane_core/helper.rb +34 -5
  90. data/fastlane_core/lib/fastlane_core/itunes_transporter.rb +0 -14
  91. data/fastlane_core/lib/fastlane_core/keychain_importer.rb +24 -27
  92. data/fastlane_core/lib/fastlane_core/languages.rb +0 -1
  93. data/fastlane_core/lib/fastlane_core/print_table.rb +5 -2
  94. data/fastlane_core/lib/fastlane_core/provisioning_profile.rb +10 -19
  95. data/fastlane_core/lib/fastlane_core/secrets.rb +77 -0
  96. data/fastlane_core/lib/fastlane_core/ui/fastlane_runner.rb +3 -3
  97. data/fastlane_core/lib/fastlane_core/ui/github_issue_inspector_reporter.rb +1 -1
  98. data/fastlane_core/lib/fastlane_core/ui/implementations/shell.rb +2 -1
  99. data/fastlane_core/lib/fastlane_core.rb +1 -0
  100. data/frameit/lib/frameit/device.rb +1 -1
  101. data/frameit/lib/frameit/frame_downloader.rb +2 -0
  102. data/frameit/lib/frameit/runner.rb +3 -2
  103. data/frameit/lib/frameit/screenshot.rb +0 -1
  104. data/gym/lib/gym/generators/build_command_generator.rb +4 -4
  105. data/gym/lib/gym/generators/package_command_generator_xcode7.rb +3 -3
  106. data/gym/lib/gym/runner.rb +1 -1
  107. data/internal/README.md +1 -1
  108. data/match/lib/match/generator.rb +1 -0
  109. data/match/lib/match/options.rb +13 -5
  110. data/match/lib/match/storage/git_storage.rb +43 -7
  111. data/match/lib/match/storage/gitlab/secure_file.rb +2 -1
  112. data/pem/lib/pem/module.rb +0 -4
  113. data/precheck/lib/precheck/rule_processor.rb +1 -0
  114. data/produce/lib/produce/itunes_connect.rb +1 -1
  115. data/produce/lib/produce/service.rb +0 -1
  116. data/produce/lib/produce.rb +0 -1
  117. data/scan/lib/scan/options.rb +7 -0
  118. data/scan/lib/scan/runner.rb +1 -1
  119. data/scan/lib/scan/test_command_generator.rb +12 -9
  120. data/scan/lib/scan/test_result_parser.rb +1 -0
  121. data/scan/lib/scan/xcpretty_reporter_options_generator.rb +2 -2
  122. data/screengrab/lib/screengrab/page.html.erb +12 -7
  123. data/screengrab/lib/screengrab/reports_generator.rb +1 -1
  124. data/sigh/lib/assets/resign.sh +17 -0
  125. data/sigh/lib/sigh/local_manage.rb +1 -1
  126. data/sigh/lib/sigh/options.rb +5 -0
  127. data/sigh/lib/sigh/resign.rb +3 -3
  128. data/sigh/lib/sigh/runner.rb +6 -1
  129. data/snapshot/lib/snapshot/page.html.erb +17 -12
  130. data/snapshot/lib/snapshot/reports_generator.rb +3 -1
  131. data/snapshot/lib/snapshot/simulator_launchers/simulator_launcher_base.rb +1 -1
  132. data/snapshot/lib/snapshot.rb +0 -2
  133. data/spaceship/README.md +2 -3
  134. data/spaceship/lib/spaceship/base.rb +3 -1
  135. data/spaceship/lib/spaceship/client.rb +65 -47
  136. data/spaceship/lib/spaceship/connect_api/models/app_screenshot_set.rb +2 -0
  137. data/spaceship/lib/spaceship/connect_api/models/profile.rb +7 -5
  138. data/spaceship/lib/spaceship/connect_api/provisioning/provisioning.rb +9 -0
  139. data/spaceship/lib/spaceship/du/upload_file.rb +1 -1
  140. data/spaceship/lib/spaceship/errors.rb +7 -1
  141. data/spaceship/lib/spaceship/globals.rb +2 -2
  142. data/spaceship/lib/spaceship/portal/merchant.rb +127 -2
  143. data/spaceship/lib/spaceship/portal/portal_client.rb +58 -0
  144. data/spaceship/lib/spaceship/portal/ui/select_team.rb +0 -2
  145. data/spaceship/lib/spaceship/portal/website_push.rb +8 -2
  146. data/spaceship/lib/spaceship/test_flight/client.rb +0 -22
  147. data/spaceship/lib/spaceship/test_flight.rb +0 -1
  148. data/spaceship/lib/spaceship/two_step_or_factor_client.rb +11 -0
  149. data/supply/lib/supply/options.rb +1 -0
  150. data/supply/lib/supply.rb +0 -1
  151. metadata +55 -37
  152. data/produce/lib/produce/available_default_languages.rb +0 -62
  153. data/snapshot/lib/snapshot/screenshot_flatten.rb +0 -20
  154. data/snapshot/lib/snapshot/screenshot_rotate.rb +0 -36
  155. data/spaceship/lib/spaceship/test_flight/app_test_info.rb +0 -40
  156. data/supply/lib/supply/apk_listing.rb +0 -14
  157. /data/fastlane/lib/assets/{custom_action_template.rb → custom_action_template.rb.template} +0 -0
@@ -605,16 +605,14 @@ module Spaceship
605
605
  # Now we know if the login is successful or if we need to do 2 factor
606
606
 
607
607
  case response.status
608
- when 403
609
- raise InvalidUserCredentialsError.new, "Invalid username and password combination. Used '#{user}' as the username."
610
608
  when 200
611
- fetch_olympus_session
609
+ fetch_olympus_session_after_signin
612
610
  return response
613
611
  when 409
614
612
  # 2 step/factor is enabled for this account, first handle that
615
613
  handle_two_step_or_factor(response)
616
614
  # and then get the olympus session
617
- fetch_olympus_session
615
+ fetch_olympus_session_after_signin
618
616
  return true
619
617
  else
620
618
  if (response.body || "").include?('invalid="true"')
@@ -692,6 +690,16 @@ module Spaceship
692
690
  return Spaceship::Hashcash.make(bits: bits, challenge: challenge)
693
691
  end
694
692
 
693
+ # Apple accepts a stale key for the sign in and 2FA, and only refuses the
694
+ # session afterwards, with a 401 that says nothing about the key.
695
+ def fetch_olympus_session_after_signin
696
+ fetch_olympus_session
697
+ rescue UnauthorizedAccessError
698
+ raise unless service_key_from_cache?
699
+
700
+ raise UnauthorizedAccessError.new, "App Store Connect refused the session after signing in. The App Store Connect API key came from the cache at #{itc_service_key_path} and may be stale: delete that file and try again."
701
+ end
702
+
695
703
  # Get the `itctx` from the new (22nd May 2017) API endpoint "olympus"
696
704
  # Update (29th March 2019) olympus migrates to new appstoreconnect API
697
705
  def fetch_olympus_session
@@ -721,58 +729,59 @@ module Spaceship
721
729
  exit(has_valid_session)
722
730
  end
723
731
 
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.
732
+ # <fastlane_user_dir>/spaceship_itc_service_key.txt: per user, and kept between
733
+ # runs. Dir.tmpdir (#30197) is shared by every user on Linux, and a literal
734
+ # "/tmp" is not a directory on Windows.
733
735
  def itc_service_key_path
734
- File.join(Dir.tmpdir, "spaceship_itc_service_key.txt")
736
+ File.join(fastlane_user_dir, "spaceship_itc_service_key.txt")
735
737
  end
736
738
 
739
+ # Read on every login from GET /logout -> 302 Location: .../signout?widgetKey=<key>.
740
+ # A stale key only fails after 2FA, so the cache is just a fallback. See fastlane#30291.
737
741
  def itc_service_key
738
742
  return @service_key if @service_key
739
743
 
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}")
744
+ key = fetch_service_key_from_signout
745
+ if key.nil?
746
+ cached = cached_service_key
747
+ return @service_key = cached if cached
748
+
749
+ key = fetch_service_key_from_olympus
746
750
  end
747
751
 
748
- @service_key = fetch_service_key
752
+ cache_service_key(key)
753
+ @service_key = key
754
+ end
749
755
 
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
756
+ def service_key_from_cache?
757
+ @service_key_from_cache == true
758
+ end
759
+
760
+ # Reading the cache is best effort: an unreadable cache is a reason to
761
+ # fetch the key again, not to fail.
762
+ def cached_service_key
763
+ return nil unless File.exist?(itc_service_key_path)
757
764
 
758
- return @service_key
765
+ key = File.read(itc_service_key_path)
766
+ @service_key_from_cache = true
767
+ logger.warn("Using the cached App Store Connect API key from #{itc_service_key_path}")
768
+ key
769
+ rescue SystemCallError => ex
770
+ logger.warn("Could not read the cached App Store Connect API key: #{ex.message}")
771
+ nil
759
772
  end
760
773
 
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
774
+ # Best effort as well: having the key and failing to save it is not a reason
775
+ # to fail the login. See fastlane#30198.
776
+ def cache_service_key(key)
777
+ File.write(itc_service_key_path, key)
778
+ rescue SystemCallError => ex
779
+ logger.warn("Could not cache the App Store Connect API key: #{ex.message}")
780
+ end
775
781
 
782
+ # The source Apple documented, removed in September 2026 and answering 404
783
+ # since. Kept as the last fallback in case it comes back. See fastlane#30199.
784
+ def fetch_service_key_from_olympus
776
785
  # Fixes issue https://github.com/fastlane/fastlane/issues/13281
777
786
  # Even though we are using https://appstoreconnect.apple.com, the service key needs to still use a
778
787
  # hostname through itunesconnect.apple.com
@@ -830,10 +839,11 @@ module Spaceship
830
839
  # said instead of the real cause.
831
840
  #
832
841
  # 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}")
842
+ # so failing here means the run is about to depend on a cached key that may
843
+ # be stale, or on an endpoint Apple has already removed once. If the
844
+ # fallback fails too, its message is all the caller sees, and this line is
845
+ # the half that says why.
846
+ logger.warn("Could not read the App Store Connect API key from the sign out redirect, falling back to the cache or the olympus endpoint: #{ex.message}")
837
847
  nil
838
848
  end
839
849
 
@@ -1189,6 +1199,11 @@ module Spaceship
1189
1199
  end
1190
1200
  end
1191
1201
 
1202
+ # signin/init, signin/complete, and the legacy signin
1203
+ def sign_in_request?(response)
1204
+ response.env.url.host == "idmsa.apple.com" && response.env.url.path.start_with?("/appleauth/auth/signin")
1205
+ end
1206
+
1192
1207
  def handle_error(response)
1193
1208
  case response.status
1194
1209
  when 401
@@ -1196,6 +1211,9 @@ module Spaceship
1196
1211
  logger.warn(msg)
1197
1212
  raise UnauthorizedAccessError.new, "Unauthorized Access"
1198
1213
  when 403
1214
+ # A refused sign-in, e.g. a locked Apple ID: like do_sirp, raise what Apple said, which with_retry does not retry. See fastlane#14387.
1215
+ raise UnexpectedResponse, response.body if sign_in_request?(response) && response.body.kind_of?(Hash) && response.body["serviceErrors"]
1216
+
1199
1217
  msg = "Access forbidden"
1200
1218
  logger.warn(msg)
1201
1219
  raise AccessForbiddenError.new, msg
@@ -19,6 +19,7 @@ module Spaceship
19
19
  APP_IPHONE_61 = "APP_IPHONE_61"
20
20
  APP_IPHONE_65 = "APP_IPHONE_65"
21
21
  APP_IPHONE_67 = "APP_IPHONE_67"
22
+ APP_IPHONE_DUO = "APP_IPHONE_DUO"
22
23
 
23
24
  APP_IPAD_97 = "APP_IPAD_97"
24
25
  APP_IPAD_105 = "APP_IPAD_105"
@@ -77,6 +78,7 @@ module Spaceship
77
78
  APP_IPHONE_61,
78
79
  APP_IPHONE_65,
79
80
  APP_IPHONE_67,
81
+ APP_IPHONE_DUO,
80
82
 
81
83
  APP_IPAD_97,
82
84
  APP_IPAD_105,
@@ -84,16 +84,18 @@ module Spaceship
84
84
  return resps.flat_map(&:to_models)
85
85
  end
86
86
 
87
- def self.create(client: nil, name: nil, profile_type: nil, bundle_id_id: nil, certificate_ids: nil, device_ids: nil)
87
+ def self.create(client: nil, name: nil, profile_type: nil, bundle_id_id: nil, certificate_ids: nil, device_ids: nil, is_offline_profile: false)
88
88
  client ||= Spaceship::ConnectAPI
89
+ attributes = {
90
+ name: name,
91
+ profileType: profile_type
92
+ }
93
+ attributes[:isOfflineProfile] = true if is_offline_profile
89
94
  resp = client.post_profiles(
90
95
  bundle_id_id: bundle_id_id,
91
96
  certificates: certificate_ids,
92
97
  devices: device_ids,
93
- attributes: {
94
- name: name,
95
- profileType: profile_type
96
- }
98
+ attributes: attributes
97
99
  )
98
100
  return resp.to_models.first
99
101
  end
@@ -3,6 +3,11 @@ require 'spaceship/connect_api/provisioning/client'
3
3
  module Spaceship
4
4
  class ConnectAPI
5
5
  module Provisioning
6
+ # Raised when a profile with 'Offline Support' is requested while authenticated
7
+ # with an App Store Connect API key. Apple only exposes the isOfflineProfile
8
+ # attribute on the Developer Portal endpoint used by Apple ID (web session) logins.
9
+ class OfflineProfileNotSupportedError < StandardError; end
10
+
6
11
  module API
7
12
  module Version
8
13
  V1 = "v1"
@@ -222,6 +227,10 @@ module Spaceship
222
227
  end
223
228
 
224
229
  def post_profiles(bundle_id_id: nil, certificates: nil, devices: nil, attributes: {})
230
+ if attributes[:isOfflineProfile] && !provisioning_request_client.web_session?
231
+ raise OfflineProfileNotSupportedError, "Profiles with 'Offline Support' (isOfflineProfile) can only be created when logged in with an Apple ID; the App Store Connect API does not support this attribute"
232
+ end
233
+
225
234
  body = {
226
235
  data: {
227
236
  attributes: attributes,
@@ -34,7 +34,7 @@ module Spaceship
34
34
  # This will copy the image into the temporary directory to remove the
35
35
  # alpha channel there. That's done to not edit the original image.
36
36
  def remove_alpha_channel(original)
37
- path = File.join(Dir.tmpdir, "#{Digest::MD5.hexdigest(original)}.png")
37
+ path = File.join(Dir.mktmpdir("spaceship-upload-"), File.basename(original))
38
38
  FileUtils.copy(original, path)
39
39
  if mac? # sips is only available on macOS
40
40
  `sips -s format bmp '#{path}' &> /dev/null` # &> /dev/null since there is warning because of the extension
@@ -86,7 +86,13 @@ module Spaceship
86
86
  end
87
87
 
88
88
  def preferred_error_info
89
- return nil unless @error_info.kind_of?(Hash) && @error_info['resultString']
89
+ return nil unless @error_info.kind_of?(Hash)
90
+
91
+ # What Apple's authentication service answers, e.g. on a locked Apple ID
92
+ service_errors = Array(@error_info['serviceErrors'])
93
+ return ["Apple provided the following error info:"] + service_errors.map { |error| [error['message'], "(#{error['code']})"].compact.join(" ") } unless service_errors.empty?
94
+
95
+ return nil unless @error_info['resultString']
90
96
 
91
97
  [
92
98
  "Apple provided the following error info:",
@@ -7,8 +7,8 @@ module Spaceship
7
7
  # if spaceship is run with a FastlaneCore available respect the global state there
8
8
  # otherwise fallback to $verbose
9
9
  def self.verbose?
10
- if Object.const_defined?("FastlaneCore")
11
- return FastlaneCore::Globals.verbose? # rubocop:disable Require/MissingRequireStatement
10
+ if Object.const_defined?("FastlaneCore") && FastlaneCore.const_defined?("Globals")
11
+ return FastlaneCore::Globals.verbose?
12
12
  end
13
13
  return $verbose
14
14
  end
@@ -46,7 +46,7 @@ module Spaceship
46
46
  # @param mac [Bool] Fetches Mac merchant if true
47
47
  # @return (Array) Returns all merchants available for this account
48
48
  def all(mac: false)
49
- client.merchants(mac: mac).map { |merchant| new(merchant) }
49
+ client.merchants(mac: mac).map { |merchant| new_with_platform(merchant, mac) }
50
50
  end
51
51
 
52
52
  # Creates a new Merchant on the Apple Dev Portal
@@ -57,7 +57,7 @@ module Spaceship
57
57
  # @return (Merchant) The Merchant you just created
58
58
  def create!(bundle_id: nil, name: nil, mac: false)
59
59
  new_merchant = client.create_merchant!(name, bundle_id, mac: mac)
60
- new(new_merchant)
60
+ new_with_platform(new_merchant, mac)
61
61
  end
62
62
 
63
63
  # Find a specific Merchant ID based on the bundle_id
@@ -68,6 +68,131 @@ module Spaceship
68
68
  merchant.bundle_id == bundle_id
69
69
  end
70
70
  end
71
+
72
+ # Apple does not return the platform, so we record the one that was asked for.
73
+ def new_with_platform(attrs, mac)
74
+ new(attrs).tap { |merchant| merchant.platform = mac ? 'mac' : 'ios' }
75
+ end
76
+ private :new_with_platform
77
+ end
78
+
79
+ # Represents a domain registered to an Apple Pay Merchant ID
80
+ class Domain < PortalBase
81
+ # @return (String) The identifier of this domain, provided by the Dev Portal
82
+ # @example
83
+ # "UFVN6788YA"
84
+ attr_accessor :domain_id
85
+
86
+ # @return (String) The domain name you registered
87
+ # @example
88
+ # "krausefx.com"
89
+ attr_accessor :name
90
+
91
+ # @return (String) Status of the domain verification
92
+ # @example
93
+ # "verified"
94
+ attr_accessor :status
95
+
96
+ # @return (String) The URL Apple expects to serve the domain association file
97
+ # @example
98
+ # "https://krausefx.com/.well-known/apple-developer-merchantid-domain-association.txt"
99
+ attr_accessor :path
100
+
101
+ # @return (Bool) Can this domain be verified by the current team?
102
+ attr_accessor :can_verify
103
+
104
+ # @return (String) The date this domain's verification expires
105
+ # @example
106
+ # "2026-10-11"
107
+ attr_accessor :expiration_date
108
+
109
+ # @return (String) A human readable version of the expiration date
110
+ # @example
111
+ # "Oct 11, 2026"
112
+ attr_accessor :expiration_date_string
113
+
114
+ # @return (Spaceship::Portal::Merchant) The merchant this domain is registered to
115
+ # This is not part of Apple's response, it gets passed in by whoever fetched the domain
116
+ attr_accessor :merchant
117
+
118
+ attr_mapping(
119
+ 'displayId' => :domain_id,
120
+ 'name' => :name,
121
+ 'status' => :status,
122
+ 'path' => :path,
123
+ 'canVerify' => :can_verify,
124
+ 'expirationDate' => :expiration_date,
125
+ 'expirationDateString' => :expiration_date_string
126
+ )
127
+
128
+ # @param attrs [Hash] The domain attributes as returned by the Dev Portal
129
+ # @param existing_client [Spaceship::Client] The client to use for requests
130
+ # @param merchant [Merchant] The merchant this domain is registered to
131
+ def initialize(attrs = {}, existing_client = nil, merchant: nil)
132
+ super(attrs, existing_client)
133
+ @merchant = merchant
134
+ end
135
+
136
+ class << self
137
+ # @param merchant [Merchant] The merchant to fetch the domains of
138
+ # @return (Array) Returns all domains registered to the given merchant
139
+ def all(merchant)
140
+ client.merchant_domains(merchant.merchant_id, mac: merchant.mac?).map do |domain|
141
+ new(domain, client, merchant: merchant)
142
+ end
143
+ end
144
+
145
+ # Registers a new domain for a Merchant ID on the Apple Dev Portal
146
+ #
147
+ # @param merchant [Merchant] the merchant to register the domain for
148
+ # @param domain_name [String] the domain name to register
149
+ # @return (Domain) The Domain you just registered
150
+ def create!(merchant: nil, domain_name: nil)
151
+ new_domain = client.create_merchant_domain!(merchant.merchant_id, domain_name, mac: merchant.mac?)
152
+ new(new_domain, client, merchant: merchant)
153
+ end
154
+
155
+ # Find a specific domain of a merchant based on the domain name
156
+ #
157
+ # @param merchant [Merchant] The merchant to search the domains of
158
+ # @param name [String] the domain name to look for
159
+ # @return (Domain) The Domain you're looking for. This is nil if the domain can't be found.
160
+ def find(merchant, name)
161
+ all(merchant).find do |domain|
162
+ domain.name == name
163
+ end
164
+ end
165
+ end
166
+
167
+ # Downloads the domain association file for this domain. It has to be
168
+ # served at `path` before Apple is able to verify the domain
169
+ #
170
+ # @return (String) The url-safe base64 encoded contents of the file
171
+ def verification_file
172
+ client.merchant_domain_get_verification_file(domain_id, mac: merchant.mac?)
173
+ end
174
+
175
+ # Asks Apple to verify this domain. The `verification_file` has to be
176
+ # served at `path` before calling this
177
+ #
178
+ # @return (Hash) The response of the Dev Portal
179
+ def verify
180
+ client.merchant_domain_verify(domain_id, mac: merchant.mac?)
181
+ end
182
+
183
+ # Delete this Domain
184
+ # @return (Domain) The domain you just deleted
185
+ def delete!
186
+ client.delete_merchant_domain!(domain_id, merchant.merchant_id, mac: merchant.mac?)
187
+ self
188
+ end
189
+ end
190
+
191
+ # @return (Array of Spaceship::Portal::Merchant::Domain) The domains registered to this merchant
192
+ def domains
193
+ @domains ||= client.merchant_domains(merchant_id, mac: mac?).map do |domain|
194
+ Domain.new(domain, client, merchant: self)
195
+ end
71
196
  end
72
197
 
73
198
  # Delete this Merchant
@@ -1,3 +1,5 @@
1
+ require 'base64'
2
+
1
3
  require_relative '../client'
2
4
 
3
5
  require_relative 'app'
@@ -344,6 +346,62 @@ module Spaceship
344
346
  end
345
347
  end
346
348
 
349
+ def merchant_domains(merchant_id, mac: false)
350
+ r = request(:post, "account/#{platform_slug(mac)}/identifiers/listDomainsForMerchant", {
351
+ merchantId: merchant_id,
352
+ teamId: team_id
353
+ })
354
+ parse_response(r, 'domainList')
355
+ end
356
+
357
+ def merchant_domain_get_verification_file(domain_id, mac: false)
358
+ r = request(:get, "account/#{platform_slug(mac)}/identifiers/downloadDomainVerificationFile", {
359
+ teamId: team_id,
360
+ domainId: domain_id
361
+ })
362
+ a = parse_response(r)
363
+ if r.success? && a.kind_of?(String)
364
+ begin
365
+ return a if Base64.urlsafe_decode64(a.delete("\r\n")).include?("Apple Inc.")
366
+ # if not valid pkcs#7 just fall through
367
+ rescue ArgumentError
368
+ end
369
+ end
370
+ raise UnexpectedResponse.new, "Couldn't download verification file, got this instead: #{a}"
371
+ end
372
+
373
+ def merchant_domain_verify(domain_id, mac: false)
374
+ ensure_csrf(Spaceship::Portal::Merchant)
375
+
376
+ r = request(:post, "account/#{platform_slug(mac)}/identifiers/verifyDomain", {
377
+ domainId: domain_id,
378
+ teamId: team_id
379
+ })
380
+ parse_response(r)
381
+ end
382
+
383
+ def create_merchant_domain!(merchant_id, domain_name, mac: false)
384
+ ensure_csrf(Spaceship::Portal::Merchant)
385
+
386
+ r = request(:post, "account/#{platform_slug(mac)}/identifiers/registerDomain", {
387
+ domainName: domain_name,
388
+ merchantId: merchant_id,
389
+ teamId: team_id
390
+ })
391
+ parse_response(r, 'domainList').first
392
+ end
393
+
394
+ def delete_merchant_domain!(domain_id, merchant_id, mac: false)
395
+ ensure_csrf(Spaceship::Portal::Merchant)
396
+
397
+ r = request(:post, "account/#{platform_slug(mac)}/identifiers/removeDomain", {
398
+ merchantId: merchant_id,
399
+ domainId: domain_id,
400
+ teamId: team_id
401
+ })
402
+ parse_response(r)
403
+ end
404
+
347
405
  def create_merchant!(name, bundle_id, mac: false)
348
406
  ensure_csrf(Spaceship::Portal::Merchant)
349
407
 
@@ -31,7 +31,6 @@ module Spaceship
31
31
  # {...}
32
32
  # ]
33
33
 
34
- # rubocop:disable Require/MissingRequireStatement
35
34
  def self.ci?
36
35
  if Object.const_defined?("FastlaneCore") && FastlaneCore.const_defined?("Helper")
37
36
  return FastlaneCore::Helper.ci?
@@ -45,7 +44,6 @@ module Spaceship
45
44
  end
46
45
  return true
47
46
  end
48
- # rubocop:enable Require/MissingRequireStatement
49
47
 
50
48
  def select_team(team_id: nil, team_name: nil)
51
49
  teams = client.teams
@@ -48,7 +48,7 @@ module Spaceship
48
48
  # @param mac [Bool] Fetches Mac website push if true
49
49
  # @return (Array) Returns all website push available for this account
50
50
  def all(mac: false)
51
- client.website_push(mac: mac).map { |website_push| self.new(website_push) }
51
+ client.website_push(mac: mac).map { |website_push| new_with_platform(website_push, mac) }
52
52
  end
53
53
 
54
54
  # Creates a new Website Push ID on the Apple Dev Portal
@@ -59,7 +59,7 @@ module Spaceship
59
59
  # @return (Website Push) The Website Push you just created
60
60
  def create!(bundle_id: nil, name: nil, mac: false)
61
61
  new_website_push = client.create_website_push!(name, bundle_id, mac: mac)
62
- self.new(new_website_push)
62
+ new_with_platform(new_website_push, mac)
63
63
  end
64
64
 
65
65
  # Find a specific Website Push ID based on the bundle_id
@@ -70,6 +70,12 @@ module Spaceship
70
70
  website_push.bundle_id == bundle_id
71
71
  end
72
72
  end
73
+
74
+ # Apple does not return the platform, so we record the one that was asked for.
75
+ def new_with_platform(attrs, mac)
76
+ new(attrs).tap { |website_push| website_push.platform = mac ? 'mac' : 'ios' }
77
+ end
78
+ private :new_with_platform
73
79
  end
74
80
 
75
81
  # Delete this Website Push ID.
@@ -286,28 +286,6 @@ module Spaceship
286
286
  handle_response(response)
287
287
  end
288
288
 
289
- ##
290
- # @!group AppTestInfo
291
- ##
292
-
293
- def get_app_test_info(app_id: nil)
294
- assert_required_params(__method__, binding)
295
-
296
- response = request(:get, "providers/#{team_id}/apps/#{app_id}/testInfo")
297
- handle_response(response)
298
- end
299
-
300
- def put_app_test_info(app_id: nil, app_test_info: nil)
301
- assert_required_params(__method__, binding)
302
-
303
- response = request(:put) do |req|
304
- req.url("providers/#{team_id}/apps/#{app_id}/testInfo")
305
- req.body = app_test_info.to_json
306
- req.headers['Content-Type'] = 'application/json'
307
- end
308
- handle_response(response)
309
- end
310
-
311
289
  protected
312
290
 
313
291
  def handle_response(response)
@@ -1,6 +1,5 @@
1
1
  require 'spaceship/test_flight/client'
2
2
  require 'spaceship/test_flight/base'
3
- require 'spaceship/test_flight/app_test_info'
4
3
  require 'spaceship/test_flight/build'
5
4
  require 'spaceship/test_flight/build_trains'
6
5
  require 'spaceship/test_flight/beta_review_info'
@@ -20,10 +20,21 @@ module Spaceship
20
20
  elsif r.body.kind_of?(Hash) && r.body["trustedPhoneNumbers"].kind_of?(Array) && r.body["trustedPhoneNumbers"].first.kind_of?(Hash)
21
21
  handle_two_factor(r)
22
22
  else
23
+ raise_two_factor_service_errors!(r.body)
23
24
  raise "Although response from Apple indicated activated Two-step Verification or Two-factor Authentication, spaceship didn't know how to handle this response: #{r.body}"
24
25
  end
25
26
  end
26
27
 
28
+ # Apple answers without any way to verify, e.g. when it cannot send the SMS code (-28248). See fastlane#29443.
29
+ def raise_two_factor_service_errors!(body)
30
+ errors = body.kind_of?(Hash) ? Array(body["serviceErrors"]) : []
31
+ return if errors.empty?
32
+
33
+ message = errors.map { |error| "#{error['message']} (#{error['code']})" }.join(" ")
34
+ message += " This Apple ID has no trusted devices: signing in to it on an Apple device adds one." if body["noTrustedDevices"]
35
+ raise Tunes::Error.new, "Apple could not start two-factor authentication: #{message}"
36
+ end
37
+
27
38
  def handle_two_step(response)
28
39
  if response.body.fetch("securityCode", {})["tooManyCodesLock"].to_s.length > 0
29
40
  raise Tunes::Error.new, "Too many verification codes have been sent. Enter the last code you received, use one of your devices, or try again later."
@@ -108,6 +108,7 @@ module Supply
108
108
  conflicting_options: [:issuer, :key, :json_key],
109
109
  optional: true,
110
110
  description: "The raw content of a Google credentials JSON file (Application Default, Workload Identity, or Service Account), used to authenticate with Google",
111
+ sensitive: true,
111
112
  code_gen_sensitive: true,
112
113
  default_value: CredentialsManager::AppfileConfig.try_fetch_value(:json_key_data_raw),
113
114
  default_value_dynamic: true,
data/supply/lib/supply.rb CHANGED
@@ -2,7 +2,6 @@ require 'json'
2
2
  require 'supply/options'
3
3
  require 'supply/client'
4
4
  require 'supply/listing'
5
- require 'supply/apk_listing'
6
5
  require 'supply/image_listing'
7
6
  require 'supply/generated_universal_apk'
8
7
  require 'supply/release_listing'