fastlane 2.156.1 → 2.157.4

Sign up to get free protection for your applications and to get access to all the features.
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