fastlane 2.156.1 → 2.157.4

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 (76) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +82 -82
  3. data/deliver/lib/deliver.rb +1 -0
  4. data/deliver/lib/deliver/app_screenshot_iterator.rb +26 -29
  5. data/deliver/lib/deliver/detect_values.rb +4 -1
  6. data/deliver/lib/deliver/languages.rb +7 -0
  7. data/deliver/lib/deliver/loader.rb +4 -5
  8. data/deliver/lib/deliver/runner.rb +8 -5
  9. data/deliver/lib/deliver/upload_screenshots.rb +34 -17
  10. data/fastlane/lib/fastlane/actions/{.git_commit.rb.swp → .ensure_git_status_clean.rb.swp} +0 -0
  11. data/fastlane/lib/fastlane/actions/.hockey.rb.swp +0 -0
  12. data/fastlane/lib/fastlane/actions/.slack.rb.swp +0 -0
  13. data/fastlane/lib/fastlane/actions/.update_project_provisioning.rb.swp +0 -0
  14. data/fastlane/lib/fastlane/actions/app_store_build_number.rb +12 -8
  15. data/fastlane/lib/fastlane/actions/app_store_connect_api_key.rb +120 -0
  16. data/fastlane/lib/fastlane/actions/commit_version_bump.rb +1 -1
  17. data/fastlane/lib/fastlane/actions/docs/upload_to_play_store.md +2 -0
  18. data/fastlane/lib/fastlane/actions/docs/upload_to_testflight.md +17 -1
  19. data/fastlane/lib/fastlane/actions/download_dsyms.rb +89 -68
  20. data/fastlane/lib/fastlane/actions/set_changelog.rb +3 -2
  21. data/fastlane/lib/fastlane/actions/sonar.rb +5 -0
  22. data/fastlane/lib/fastlane/actions/spaceship_stats.rb +73 -0
  23. data/fastlane/lib/fastlane/actions/upload_to_testflight.rb +4 -0
  24. data/fastlane/lib/fastlane/version.rb +1 -1
  25. data/fastlane/swift/Deliverfile.swift +1 -1
  26. data/fastlane/swift/DeliverfileProtocol.swift +1 -1
  27. data/fastlane/swift/Fastlane.swift +78 -18
  28. data/fastlane/swift/FastlaneSwiftRunner/FastlaneSwiftRunner.xcodeproj/project.xcworkspace/xcuserdata/josh.xcuserdatad/UserInterfaceState.xcuserstate +0 -0
  29. data/fastlane/swift/Gymfile.swift +1 -1
  30. data/fastlane/swift/GymfileProtocol.swift +1 -1
  31. data/fastlane/swift/Matchfile.swift +1 -1
  32. data/fastlane/swift/MatchfileProtocol.swift +1 -1
  33. data/fastlane/swift/Precheckfile.swift +1 -1
  34. data/fastlane/swift/PrecheckfileProtocol.swift +1 -1
  35. data/fastlane/swift/Scanfile.swift +1 -1
  36. data/fastlane/swift/ScanfileProtocol.swift +1 -1
  37. data/fastlane/swift/Screengrabfile.swift +1 -1
  38. data/fastlane/swift/ScreengrabfileProtocol.swift +1 -1
  39. data/fastlane/swift/Snapshotfile.swift +1 -1
  40. data/fastlane/swift/SnapshotfileProtocol.swift +1 -1
  41. data/fastlane_core/lib/fastlane_core/command_executor.rb +1 -0
  42. data/fastlane_core/lib/fastlane_core/itunes_transporter.rb +71 -42
  43. data/gym/lib/gym/error_handler.rb +1 -1
  44. data/match/lib/match/spaceship_ensure.rb +5 -5
  45. data/{fastlane/lib/fastlane/.erb_template_helper.rb.swp → pilot/lib/pilot/.manager.rb.swp} +0 -0
  46. data/pilot/lib/pilot/build_manager.rb +15 -4
  47. data/pilot/lib/pilot/manager.rb +15 -5
  48. data/pilot/lib/pilot/options.rb +16 -0
  49. data/produce/lib/produce/itunes_connect.rb +3 -2
  50. data/screengrab/lib/screengrab/runner.rb +29 -10
  51. data/sigh/lib/assets/resign.sh +9 -6
  52. data/sigh/lib/sigh/runner.rb +5 -4
  53. data/spaceship/lib/spaceship.rb +4 -0
  54. data/spaceship/lib/spaceship/client.rb +3 -0
  55. data/spaceship/lib/spaceship/connect_api.rb +1 -15
  56. data/spaceship/lib/spaceship/{.DS_Store → connect_api/.DS_Store} +0 -0
  57. data/spaceship/lib/spaceship/connect_api/api_client.rb +270 -0
  58. data/spaceship/lib/spaceship/connect_api/client.rb +155 -210
  59. data/spaceship/lib/spaceship/connect_api/file_uploader.rb +2 -0
  60. data/spaceship/lib/spaceship/connect_api/models/app.rb +5 -5
  61. data/spaceship/lib/spaceship/connect_api/models/app_price_point.rb +26 -0
  62. data/spaceship/lib/spaceship/connect_api/models/app_store_version.rb +7 -1
  63. data/spaceship/lib/spaceship/connect_api/provisioning/client.rb +8 -17
  64. data/spaceship/lib/spaceship/connect_api/provisioning/provisioning.rb +75 -64
  65. data/spaceship/lib/spaceship/connect_api/spaceship.rb +98 -0
  66. data/spaceship/lib/spaceship/connect_api/testflight/client.rb +8 -17
  67. data/spaceship/lib/spaceship/connect_api/testflight/testflight.rb +288 -277
  68. data/spaceship/lib/spaceship/connect_api/token.rb +46 -5
  69. data/spaceship/lib/spaceship/connect_api/token_refresh_middleware.rb +24 -0
  70. data/spaceship/lib/spaceship/connect_api/tunes/client.rb +8 -17
  71. data/spaceship/lib/spaceship/connect_api/tunes/tunes.rb +725 -706
  72. data/spaceship/lib/spaceship/connect_api/users/client.rb +8 -17
  73. data/spaceship/lib/spaceship/connect_api/users/users.rb +28 -17
  74. data/spaceship/lib/spaceship/stats_middleware.rb +65 -0
  75. metadata +33 -22
  76. data/spaceship/lib/spaceship/connect_api/.client.rb.swp +0 -0
@@ -143,7 +143,7 @@ module Gym
143
143
  # `xcodebuild` doesn't properly mark lines as failure reason or important information
144
144
  # so we assume that the last few lines show the error message that's relevant
145
145
  # (at least that's what was correct during testing)
146
- log_content = File.read(log_path).split("\n")[-5..-1]
146
+ log_content = File.read(log_path).split("\n").last(5)
147
147
  log_content.each do |row|
148
148
  UI.command_output(row)
149
149
  end
@@ -18,18 +18,18 @@ module Match
18
18
  UI.important("More information https://docs.fastlane.tools/actions/match/#access-control")
19
19
  end
20
20
 
21
+ # Prompts select team if multiple teams and none specified
21
22
  UI.message("Verifying that the certificate and profile are still valid on the Dev Portal...")
22
- Spaceship.login(user)
23
- Spaceship.select_team(team_id: team_id, team_name: team_name)
23
+ Spaceship::ConnectAPI.login(user, use_portal: true, use_tunes: false, portal_team_id: team_id, team_name: team_name)
24
24
  end
25
25
 
26
26
  # The team ID of the currently logged in team
27
27
  def team_id
28
- return Spaceship.client.team_id
28
+ return Spaceship::ConnectAPI.client.portal_team_id
29
29
  end
30
30
 
31
31
  def bundle_identifier_exists(username: nil, app_identifier: nil, platform: nil)
32
- found = Spaceship.app.find(app_identifier, mac: platform == "macos")
32
+ found = Spaceship::ConnectAPI::BundleId.find(app_identifier)
33
33
  return if found
34
34
 
35
35
  require 'sigh/runner'
@@ -39,7 +39,7 @@ module Match
39
39
  })
40
40
  UI.error("An app with that bundle ID needs to exist in order to create a provisioning profile for it")
41
41
  UI.error("================================================================")
42
- available_apps = Spaceship.app.all.collect { |a| "#{a.bundle_id} (#{a.name})" }
42
+ available_apps = Spaceship::ConnectAPI::BundleId.all.collect { |a| "#{a.identifier} (#{a.name})" }
43
43
  UI.message("Available apps:\n- #{available_apps.join("\n- ")}")
44
44
  UI.error("Make sure to run `fastlane match` with the same user and team every time.")
45
45
  UI.user_error!("Couldn't find bundle identifier '#{app_identifier}' for the user '#{username}'")
@@ -343,22 +343,33 @@ module Pilot
343
343
  builds_to_expire.each(&:expire!)
344
344
  end
345
345
 
346
+ # If App Store Connect API token, use token.
346
347
  # If itc_provider was explicitly specified, use it.
347
348
  # If there are multiple teams, infer the provider from the selected team name.
348
349
  # If there are fewer than two teams, don't infer the provider.
349
350
  def transporter_for_selected_team(options)
351
+ # Use JWT auth
352
+ unless api_token.nil?
353
+ api_token.refresh! if api_token.expired?
354
+ return FastlaneCore::ItunesTransporter.new(nil, nil, false, nil, api_token.text)
355
+ end
356
+
357
+ # Otherwise use username and password
358
+ tunes_client = Spaceship::ConnectAPI.client ? Spaceship::ConnectAPI.client.tunes_client : nil
359
+
350
360
  generic_transporter = FastlaneCore::ItunesTransporter.new(options[:username], nil, false, options[:itc_provider])
351
- return generic_transporter if options[:itc_provider] || Spaceship::Tunes.client.nil?
352
- return generic_transporter unless Spaceship::Tunes.client.teams.count > 1
361
+ return generic_transporter if options[:itc_provider] || tunes_client.nil?
362
+ return generic_transporter unless tunes_client.teams.count > 1
353
363
 
354
364
  begin
355
- team = Spaceship::Tunes.client.teams.find { |t| t['contentProvider']['contentProviderId'].to_s == Spaceship::Tunes.client.team_id }
365
+ team = tunes_client.teams.find { |t| t['contentProvider']['contentProviderId'].to_s == tunes_client.team_id }
356
366
  name = team['contentProvider']['name']
357
367
  provider_id = generic_transporter.provider_ids[name]
358
368
  UI.verbose("Inferred provider id #{provider_id} for team #{name}.")
359
369
  return FastlaneCore::ItunesTransporter.new(options[:username], nil, false, provider_id)
360
370
  rescue => ex
361
- UI.verbose("Couldn't infer a provider short name for team with id #{Spaceship::Tunes.client.team_id} automatically: #{ex}. Proceeding without provider short name.")
371
+ STDERR.puts(ex.to_s)
372
+ UI.verbose("Couldn't infer a provider short name for team with id #{tunes_client.team_id} automatically: #{ex}. Proceeding without provider short name.")
362
373
  return generic_transporter
363
374
  end
364
375
  end
@@ -17,12 +17,22 @@ module Pilot
17
17
  end
18
18
 
19
19
  def login
20
- config[:username] ||= CredentialsManager::AppfileConfig.try_fetch_value(:apple_id)
20
+ if api_token
21
+ UI.message("Creating authorization token for App Store Connect API")
22
+ Spaceship::ConnectAPI.token = api_token
23
+ else
24
+ config[:username] ||= CredentialsManager::AppfileConfig.try_fetch_value(:apple_id)
21
25
 
22
- UI.message("Login to App Store Connect (#{config[:username]})")
23
- Spaceship::Tunes.login(config[:username])
24
- Spaceship::Tunes.select_team(team_id: config[:team_id], team_name: config[:team_name])
25
- UI.message("Login successful")
26
+ UI.message("Login to App Store Connect (#{config[:username]})")
27
+ Spaceship::ConnectAPI.login(config[:username], use_portal: false, use_tunes: true, tunes_team_id: config[:team_id], team_name: config[:team_name])
28
+ UI.message("Login successful")
29
+ end
30
+ end
31
+
32
+ def api_token
33
+ @api_token ||= Spaceship::ConnectAPI::Token.create(config[:api_key]) if config[:api_key]
34
+ @api_token ||= Spaceship::ConnectAPI::Token.from_json_file(config[:api_key_path]) if config[:api_key_path]
35
+ return @api_token
26
36
  end
27
37
 
28
38
  # The app object we're currently using
@@ -10,6 +10,22 @@ module Pilot
10
10
  user ||= CredentialsManager::AppfileConfig.try_fetch_value(:apple_id)
11
11
 
12
12
  [
13
+ FastlaneCore::ConfigItem.new(key: :api_key_path,
14
+ env_name: "PILOT_API_KEY_PATH",
15
+ description: "Path to your App Store Connect API key JSON file",
16
+ optional: true,
17
+ conflicting_options: [:username],
18
+ verify_block: proc do |value|
19
+ UI.user_error!("Couldn't find API key JSON file at path '#{value}'") unless File.exist?(value)
20
+ end),
21
+ FastlaneCore::ConfigItem.new(key: :api_key,
22
+ env_name: "PILOT_API_KEY",
23
+ description: "Path to your App Store Connect API key JSON file",
24
+ type: Hash,
25
+ optional: true,
26
+ sensitive: true,
27
+ conflicting_options: [:api_key_path, :username]),
28
+
13
29
  # app upload info
14
30
  FastlaneCore::ConfigItem.new(key: :username,
15
31
  short_option: "-u",
@@ -9,8 +9,9 @@ module Produce
9
9
  @full_bundle_identifier = app_identifier
10
10
  @full_bundle_identifier.gsub!('*', Produce.config[:bundle_identifier_suffix].to_s) if wildcard_bundle?
11
11
 
12
- Spaceship::Tunes.login(Produce.config[:username], nil)
13
- Spaceship::Tunes.client.select_team
12
+ # Team selection passed though FASTLANE_ITC_TEAM_ID and FASTLANE_ITC_TEAM_NAME environment variables
13
+ # Prompts select team if multiple teams and none specified
14
+ Spaceship::ConnectAPI.login(Produce.config[:username], nil, use_portal: false, use_tunes: true)
14
15
 
15
16
  create_new_app
16
17
  end
@@ -64,6 +64,7 @@ module Screengrab
64
64
  # Root is needed to access device paths at /data
65
65
  if @config[:use_adb_root]
66
66
  run_adb_command("-s #{device_serial} root", print_all: false, print_command: true)
67
+ run_adb_command("-s #{device_serial} wait-for-device", print_all: false, print_command: true)
67
68
  end
68
69
 
69
70
  clear_device_previous_screenshots(@config[:app_package_name], device_serial, device_screenshots_paths)
@@ -81,7 +82,7 @@ module Screengrab
81
82
  end
82
83
 
83
84
  def select_device
84
- adb = Fastlane::Helper::AdbHelper.new(adb_host: @config[:adb_host])
85
+ adb = Fastlane::Helper::AdbHelper.new(adb_path: @android_env.adb_path, adb_host: @config[:adb_host])
85
86
  devices = adb.load_all_devices
86
87
 
87
88
  UI.user_error!('There are no connected and authorized devices or emulators') if devices.empty?
@@ -298,9 +299,17 @@ module Screengrab
298
299
  device_screenshots_paths.each do |device_path|
299
300
  if_device_path_exists(@config[:app_package_name], device_serial, device_path) do |path|
300
301
  next unless path.include?(locale)
301
- run_adb_command("-s #{device_serial} pull #{path} #{tempdir}",
302
- print_all: false,
303
- print_command: true)
302
+ out = run_adb_command("-s #{device_serial} pull #{path} #{tempdir}",
303
+ print_all: false,
304
+ print_command: true,
305
+ raise_errors: false)
306
+ if out =~ /Permission denied/
307
+ dir = File.dirname(path)
308
+ base = File.basename(path)
309
+ run_adb_command("-s #{device_serial} shell run-as #{@config[:app_package_name]} 'tar -cC #{dir} #{base}' | tar -xvC #{tempdir}",
310
+ print_all: false,
311
+ print_command: true)
312
+ end
304
313
  end
305
314
  end
306
315
 
@@ -380,16 +389,26 @@ module Screengrab
380
389
  packages.split("\n").map { |package| package.gsub("package:", "") }
381
390
  end
382
391
 
383
- def run_adb_command(command, print_all: false, print_command: false)
392
+ def run_adb_command(command, print_all: false, print_command: false, raise_errors: true)
384
393
  adb_path = @android_env.adb_path.chomp("adb")
385
394
  adb_host = @config[:adb_host]
386
395
  host = adb_host.nil? ? '' : "-H #{adb_host} "
387
- output = @executor.execute(command: adb_path + "adb " + host + command,
388
- print_all: print_all,
389
- print_command: print_command) || ''
396
+ output = ''
397
+ begin
398
+ errout = nil
399
+ cmdout = @executor.execute(command: adb_path + "adb " + host + command,
400
+ print_all: print_all,
401
+ print_command: print_command,
402
+ error: raise_errors ? nil : proc { |out, status| errout = out }) || ''
403
+ output = errout || cmdout
404
+ rescue => ex
405
+ if raise_errors
406
+ raise ex
407
+ end
408
+ end
390
409
  output.lines.reject do |line|
391
- # Debug/Warning output from ADB}
392
- line.start_with?('adb: ')
410
+ # Debug/Warning output from ADB
411
+ line.start_with?('adb: ') && !line.start_with?('adb: error: ')
393
412
  end.join('') # Lines retain their newline chars
394
413
  end
395
414
 
@@ -72,6 +72,9 @@
72
72
  # new features June 2020
73
73
  # 1. enable (re)signing of OnDemandResources when ipa has been built for the appstore
74
74
  #
75
+ # new features August 2020
76
+ # 1. fixes usage for users with GNU-sed in their $PATH
77
+ #
75
78
 
76
79
  # Logging functions
77
80
 
@@ -762,7 +765,7 @@ function resign {
762
765
 
763
766
  # Get the entry from app's entitlements
764
767
  # Read it with PlistBuddy as XML, then strip the header and <plist></plist> part
765
- ENTITLEMENTS_VALUE="$(PlistBuddy -x -c "Print $KEY" "$APP_ENTITLEMENTS" 2>/dev/null | sed -e 's,.*<plist[^>]*>\(.*\)</plist>,\1,g')"
768
+ ENTITLEMENTS_VALUE="$(PlistBuddy -x -c "Print $KEY" "$APP_ENTITLEMENTS" 2>/dev/null | /usr/bin/sed -e 's,.*<plist[^>]*>\(.*\)</plist>,\1,g')"
766
769
  if [[ -z "$ENTITLEMENTS_VALUE" ]]; then
767
770
  log "No value for '$KEY'"
768
771
  continue
@@ -780,7 +783,7 @@ function resign {
780
783
  log "Certificate $CERTIFICATE matches a SHA1 pattern"
781
784
  local certificate_matches="$( security find-identity -v -p codesigning | grep -m 1 "$CERTIFICATE" )"
782
785
  if [ -n "$certificate_matches" ]; then
783
- certificate_name="$( sed -E s/[^\"]+\"\([^\"]+\)\".*/\\1/ <<< $certificate_matches )"
786
+ certificate_name="$(/usr/bin/sed -E s/[^\"]+\"\([^\"]+\)\".*/\\1/ <<< $certificate_matches )"
784
787
  log "Certificate name: $certificate_name"
785
788
  fi
786
789
  fi
@@ -807,18 +810,18 @@ function resign {
807
810
  # otherwise it interprets they key path as nested keys
808
811
  # TODO: Should be able to replace with echo ${KEY//\./\\\\.} and remove shellcheck disable directive
809
812
  # shellcheck disable=SC2001
810
- PLUTIL_KEY=$(echo "$KEY" | sed 's/\./\\\./g')
813
+ PLUTIL_KEY=$(echo "$KEY" | /usr/bin/sed 's/\./\\\./g')
811
814
  plutil -insert "$PLUTIL_KEY" -xml "$ENTITLEMENTS_VALUE" "$PATCHED_ENTITLEMENTS"
812
815
 
813
816
  # Patch the ID value if specified
814
817
  if [[ "$ID_TYPE" == "APP_ID" ]]; then
815
818
  # Replace old value with new value in patched entitlements
816
819
  log "Replacing old app identifier prefix '$OLD_APP_ID' with new value '$NEW_APP_ID'"
817
- sed -i .bak "s/$OLD_APP_ID/$NEW_APP_ID/g" "$PATCHED_ENTITLEMENTS"
820
+ /usr/bin/sed -i .bak "s/$OLD_APP_ID/$NEW_APP_ID/g" "$PATCHED_ENTITLEMENTS"
818
821
  elif [[ "$ID_TYPE" == "TEAM_ID" ]]; then
819
822
  # Replace old team identifier with new value
820
823
  log "Replacing old team ID '$OLD_TEAM_ID' with new team ID: '$NEW_TEAM_ID'"
821
- sed -i .bak "s/$OLD_TEAM_ID/$NEW_TEAM_ID/g" "$PATCHED_ENTITLEMENTS"
824
+ /usr/bin/sed -i .bak "s/$OLD_TEAM_ID/$NEW_TEAM_ID/g" "$PATCHED_ENTITLEMENTS"
822
825
  else
823
826
  continue
824
827
  fi
@@ -835,7 +838,7 @@ function resign {
835
838
  # e.g. <string>AB1GP98Q19.com.example.foo</string>
836
839
  # vs
837
840
  # com.example.foo
838
- sed -i .bak "s!${OLD_BUNDLE_ID}</string>!${NEW_BUNDLE_ID}</string>!g" "$PATCHED_ENTITLEMENTS"
841
+ /usr/bin/sed -i .bak "s!${OLD_BUNDLE_ID}</string>!${NEW_BUNDLE_ID}</string>!g" "$PATCHED_ENTITLEMENTS"
839
842
 
840
843
  log "Resigning application using certificate: '$CERTIFICATE'"
841
844
  log "and patched entitlements:"
@@ -17,9 +17,10 @@ module Sigh
17
17
  hide_keys: [:output_path],
18
18
  title: "Summary for sigh #{Fastlane::VERSION}")
19
19
 
20
+ # Team selection passed though FASTLANE_ITC_TEAM_ID and FASTLANE_ITC_TEAM_NAME environment variables
21
+ # Prompts select team if multiple teams and none specified
20
22
  UI.message("Starting login with user '#{Sigh.config[:username]}'")
21
- Spaceship.login(Sigh.config[:username], nil)
22
- Spaceship.select_team
23
+ Spaceship::ConnectAPI.login(Sigh.config[:username], nil, use_portal: true, use_tunes: false)
23
24
  UI.message("Successfully logged in")
24
25
 
25
26
  profiles = [] if Sigh.config[:skip_fetch_profiles]
@@ -60,12 +61,12 @@ module Sigh
60
61
  case Sigh.config[:platform]
61
62
  when "ios"
62
63
  @profile_type = Spaceship::ConnectAPI::Profile::ProfileType::IOS_APP_STORE
63
- @profile_type = Spaceship::ConnectAPI::Profile::ProfileType::IOS_APP_INHOUSE if Spaceship.client.in_house?
64
+ @profile_type = Spaceship::ConnectAPI::Profile::ProfileType::IOS_APP_INHOUSE if Spaceship::ConnectAPI.client.in_house?
64
65
  @profile_type = Spaceship::ConnectAPI::Profile::ProfileType::IOS_APP_ADHOC if Sigh.config[:adhoc]
65
66
  @profile_type = Spaceship::ConnectAPI::Profile::ProfileType::IOS_APP_DEVELOPMENT if Sigh.config[:development]
66
67
  when "tvos"
67
68
  @profile_type = Spaceship::ConnectAPI::Profile::ProfileType::TVOS_APP_STORE
68
- @profile_type = Spaceship::ConnectAPI::Profile::ProfileType::TVOS_APP_INHOUSE if Spaceship.client.in_house?
69
+ @profile_type = Spaceship::ConnectAPI::Profile::ProfileType::TVOS_APP_INHOUSE if Spaceship::ConnectAPI.client.in_house?
69
70
  @profile_type = Spaceship::ConnectAPI::Profile::ProfileType::TVOS_APP_ADHOC if Sigh.config[:adhoc]
70
71
  @profile_type = Spaceship::ConnectAPI::Profile::ProfileType::TVOS_APP_DEVELOPMENT if Sigh.config[:development]
71
72
  when "macos"
@@ -4,6 +4,9 @@ require_relative 'spaceship/client'
4
4
  require_relative 'spaceship/provider'
5
5
  require_relative 'spaceship/launcher'
6
6
 
7
+ # Middleware
8
+ require_relative 'spaceship/stats_middleware'
9
+
7
10
  # Dev Portal
8
11
  require_relative 'spaceship/portal/portal'
9
12
  require_relative 'spaceship/portal/spaceship'
@@ -13,6 +16,7 @@ require_relative 'spaceship/tunes/tunes'
13
16
  require_relative 'spaceship/tunes/spaceship'
14
17
  require_relative 'spaceship/test_flight'
15
18
  require_relative 'spaceship/connect_api'
19
+ require_relative 'spaceship/connect_api/spaceship'
16
20
  require_relative 'spaceship/spaceauth_runner'
17
21
 
18
22
  require_relative 'spaceship/module'
@@ -6,6 +6,7 @@ require 'logger'
6
6
  require 'tmpdir'
7
7
  require 'cgi'
8
8
  require 'tempfile'
9
+ require 'openssl'
9
10
 
10
11
  require 'fastlane/version'
11
12
  require_relative 'helper/net_http_generic_request'
@@ -16,6 +17,7 @@ require_relative 'errors'
16
17
  require_relative 'tunes/errors'
17
18
  require_relative 'globals'
18
19
  require_relative 'provider'
20
+ require_relative 'stats_middleware'
19
21
 
20
22
  Faraday::Utils.default_params_encoder = Faraday::FlatParamsEncoder
21
23
 
@@ -209,6 +211,7 @@ module Spaceship
209
211
  c.response(:plist, content_type: /\bplist$/)
210
212
  c.use(:cookie_jar, jar: @cookie)
211
213
  c.use(FaradayMiddleware::RelsMiddleware)
214
+ c.use(Spaceship::StatsMiddleware)
212
215
  c.adapter(Faraday.default_adapter)
213
216
 
214
217
  if ENV['SPACESHIP_DEBUG']
@@ -39,6 +39,7 @@ require 'spaceship/connect_api/models/app_info_localization'
39
39
  require 'spaceship/connect_api/models/app_preview_set'
40
40
  require 'spaceship/connect_api/models/app_preview'
41
41
  require 'spaceship/connect_api/models/app_price'
42
+ require 'spaceship/connect_api/models/app_price_point'
42
43
  require 'spaceship/connect_api/models/app_price_tier'
43
44
  require 'spaceship/connect_api/models/app_store_review_attachment'
44
45
  require 'spaceship/connect_api/models/app_store_review_detail'
@@ -56,21 +57,6 @@ require 'spaceship/connect_api/models/territory'
56
57
 
57
58
  module Spaceship
58
59
  class ConnectAPI
59
- extend Spaceship::ConnectAPI::Provisioning
60
- extend Spaceship::ConnectAPI::TestFlight
61
- extend Spaceship::ConnectAPI::Users
62
- extend Spaceship::ConnectAPI::Tunes
63
-
64
- @token = nil
65
-
66
- class << self
67
- attr_writer(:token)
68
- end
69
-
70
- class << self
71
- attr_reader :token
72
- end
73
-
74
60
  # Defined in the App Store Connect API docs:
75
61
  # https://developer.apple.com/documentation/appstoreconnectapi/platform
76
62
  #
@@ -0,0 +1,270 @@
1
+
2
+ require_relative '../client'
3
+ require_relative './response'
4
+ require_relative '../client'
5
+ require_relative './response'
6
+ require_relative './token_refresh_middleware'
7
+
8
+ require_relative '../stats_middleware'
9
+
10
+ module Spaceship
11
+ class ConnectAPI
12
+ class APIClient < Spaceship::Client
13
+ attr_accessor :token
14
+
15
+ #####################################################
16
+ # @!group Client Init
17
+ #####################################################
18
+
19
+ # Instantiates a client with cookie session or a JWT token.
20
+ def initialize(cookie: nil, current_team_id: nil, token: nil, another_client: nil)
21
+ params_count = [cookie, token, another_client].compact.size
22
+ if params_count != 1
23
+ raise "Must initialize with one of :cookie, :token, or :another_client"
24
+ end
25
+
26
+ if token.nil?
27
+ if another_client.nil?
28
+ super(cookie: cookie, current_team_id: current_team_id, timeout: 1200)
29
+ return
30
+ end
31
+ super(cookie: another_client.instance_variable_get(:@cookie), current_team_id: another_client.team_id)
32
+ else
33
+ options = {
34
+ request: {
35
+ timeout: (ENV["SPACESHIP_TIMEOUT"] || 300).to_i,
36
+ open_timeout: (ENV["SPACESHIP_TIMEOUT"] || 300).to_i
37
+ }
38
+ }
39
+ @token = token
40
+ @current_team_id = current_team_id
41
+
42
+ @client = Faraday.new(hostname, options) do |c|
43
+ c.response(:json, content_type: /\bjson$/)
44
+ c.response(:plist, content_type: /\bplist$/)
45
+ c.use(FaradayMiddleware::RelsMiddleware)
46
+ c.use(Spaceship::StatsMiddleware)
47
+ c.use(Spaceship::TokenRefreshMiddleware, token)
48
+ c.adapter(Faraday.default_adapter)
49
+
50
+ if ENV['SPACESHIP_DEBUG']
51
+ # for debugging only
52
+ # This enables tracking of networking requests using Charles Web Proxy
53
+ c.proxy = "https://127.0.0.1:8888"
54
+ c.ssl[:verify_mode] = OpenSSL::SSL::VERIFY_NONE
55
+ elsif ENV["SPACESHIP_PROXY"]
56
+ c.proxy = ENV["SPACESHIP_PROXY"]
57
+ c.ssl[:verify_mode] = OpenSSL::SSL::VERIFY_NONE if ENV["SPACESHIP_PROXY_SSL_VERIFY_NONE"]
58
+ end
59
+
60
+ if ENV["DEBUG"]
61
+ puts("To run spaceship through a local proxy, use SPACESHIP_DEBUG")
62
+ end
63
+ end
64
+ end
65
+ end
66
+
67
+ # Instance level hostname only used when creating
68
+ # App Store Connect API Farady client.
69
+ # Forwarding to class level if using web session.
70
+ def hostname
71
+ if @token
72
+ return "https://api.appstoreconnect.apple.com/v1/"
73
+ end
74
+ return self.class.hostname
75
+ end
76
+
77
+ def self.hostname
78
+ # Implemented in subclass
79
+ not_implemented(__method__)
80
+ end
81
+
82
+ #
83
+ # Helpers
84
+ #
85
+
86
+ def web_session?
87
+ return @token.nil?
88
+ end
89
+
90
+ def build_params(filter: nil, includes: nil, limit: nil, sort: nil, cursor: nil)
91
+ params = {}
92
+
93
+ filter = filter.delete_if { |k, v| v.nil? } if filter
94
+
95
+ params[:filter] = filter if filter && !filter.empty?
96
+ params[:include] = includes if includes
97
+ params[:limit] = limit if limit
98
+ params[:sort] = sort if sort
99
+ params[:cursor] = cursor if cursor
100
+
101
+ return params
102
+ end
103
+
104
+ def get(url_or_path, params = nil)
105
+ response = with_asc_retry do
106
+ request(:get) do |req|
107
+ req.url(url_or_path)
108
+ req.options.params_encoder = Faraday::NestedParamsEncoder
109
+ req.params = params if params
110
+ req.headers['Content-Type'] = 'application/json'
111
+ end
112
+ end
113
+ handle_response(response)
114
+ end
115
+
116
+ def post(url_or_path, body, tries: 5)
117
+ response = with_asc_retry(tries) do
118
+ request(:post) do |req|
119
+ req.url(url_or_path)
120
+ req.body = body.to_json
121
+ req.headers['Content-Type'] = 'application/json'
122
+ end
123
+ end
124
+ handle_response(response)
125
+ end
126
+
127
+ def patch(url_or_path, body)
128
+ response = with_asc_retry do
129
+ request(:patch) do |req|
130
+ req.url(url_or_path)
131
+ req.body = body.to_json
132
+ req.headers['Content-Type'] = 'application/json'
133
+ end
134
+ end
135
+ handle_response(response)
136
+ end
137
+
138
+ def delete(url_or_path, params = nil, body = nil)
139
+ response = with_asc_retry do
140
+ request(:delete) do |req|
141
+ req.url(url_or_path)
142
+ req.options.params_encoder = Faraday::NestedParamsEncoder if params
143
+ req.params = params if params
144
+ req.body = body.to_json if body
145
+ req.headers['Content-Type'] = 'application/json' if body
146
+ end
147
+ end
148
+ handle_response(response)
149
+ end
150
+
151
+ protected
152
+
153
+ def with_asc_retry(tries = 5, &_block)
154
+ tries = 1 if Object.const_defined?("SpecHelper")
155
+ response = yield
156
+
157
+ status = response.status if response
158
+
159
+ if [500, 504].include?(status)
160
+ msg = "Timeout received! Retrying after 3 seconds (remaining: #{tries})..."
161
+ raise msg
162
+ end
163
+
164
+ return response
165
+ rescue => error
166
+ tries -= 1
167
+ puts(error) if Spaceship::Globals.verbose?
168
+ if tries.zero?
169
+ return response
170
+ else
171
+ retry
172
+ end
173
+ end
174
+
175
+ def handle_response(response)
176
+ if (200...300).cover?(response.status) && (response.body.nil? || response.body.empty?)
177
+ return
178
+ end
179
+
180
+ raise InternalServerError, "Server error got #{response.status}" if (500...600).cover?(response.status)
181
+
182
+ unless response.body.kind_of?(Hash)
183
+ raise UnexpectedResponse, response.body
184
+ end
185
+
186
+ raise UnexpectedResponse, response.body['error'] if response.body['error']
187
+
188
+ raise UnexpectedResponse, handle_errors(response) if response.body['errors']
189
+
190
+ raise UnexpectedResponse, "Temporary App Store Connect error: #{response.body}" if response.body['statusCode'] == 'ERROR'
191
+
192
+ store_csrf_tokens(response)
193
+
194
+ return Spaceship::ConnectAPI::Response.new(body: response.body, status: response.status, client: self)
195
+ end
196
+
197
+ def handle_errors(response)
198
+ # Example error format
199
+ # {
200
+ # "errors":[
201
+ # {
202
+ # "id":"cbfd8674-4802-4857-bfe8-444e1ea36e32",
203
+ # "status":"409",
204
+ # "code":"STATE_ERROR",
205
+ # "title":"The request cannot be fulfilled because of the state of another resource.",
206
+ # "detail":"Submit for review errors found.",
207
+ # "meta":{
208
+ # "associatedErrors":{
209
+ # "/v1/appScreenshots/":[
210
+ # {
211
+ # "id":"23d1734f-b81f-411a-98e4-6d3e763d54ed",
212
+ # "status":"409",
213
+ # "code":"STATE_ERROR.SCREENSHOT_REQUIRED.APP_WATCH_SERIES_4",
214
+ # "title":"App screenshot missing (APP_WATCH_SERIES_4)."
215
+ # },
216
+ # {
217
+ # "id":"db993030-0a93-48e9-9fd7-7e5676633431",
218
+ # "status":"409",
219
+ # "code":"STATE_ERROR.SCREENSHOT_REQUIRED.APP_WATCH_SERIES_4",
220
+ # "title":"App screenshot missing (APP_WATCH_SERIES_4)."
221
+ # }
222
+ # ],
223
+ # "/v1/builds/d710b6fa-5235-4fe4-b791-2b80d6818db0":[
224
+ # {
225
+ # "id":"e421fe6f-0e3b-464b-89dc-ba437e7bb77d",
226
+ # "status":"409",
227
+ # "code":"ENTITY_ERROR.ATTRIBUTE.REQUIRED",
228
+ # "title":"The provided entity is missing a required attribute",
229
+ # "detail":"You must provide a value for the attribute 'usesNonExemptEncryption' with this request",
230
+ # "source":{
231
+ # "pointer":"/data/attributes/usesNonExemptEncryption"
232
+ # }
233
+ # }
234
+ # ]
235
+ # }
236
+ # }
237
+ # }
238
+ # ]
239
+ # }
240
+
241
+ return response.body['errors'].map do |error|
242
+ messages = [[error['title'], error['detail']].compact.join(" - ")]
243
+
244
+ meta = error["meta"] || {}
245
+ associated_errors = meta["associatedErrors"] || {}
246
+
247
+ messages + associated_errors.values.flatten.map do |associated_error|
248
+ [[associated_error["title"], associated_error["detail"]].compact.join(" - ")]
249
+ end
250
+ end.flatten.join("\n")
251
+ end
252
+
253
+ private
254
+
255
+ def local_variable_get(binding, name)
256
+ if binding.respond_to?(:local_variable_get)
257
+ binding.local_variable_get(name)
258
+ else
259
+ binding.eval(name.to_s)
260
+ end
261
+ end
262
+
263
+ def provider_id
264
+ return team_id if self.provider.nil?
265
+ self.provider.provider_id
266
+ end
267
+ end
268
+ end
269
+ # rubocop:enable Metrics/ClassLength
270
+ end