fastlane 2.155.1 → 2.157.0
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.
- checksums.yaml +4 -4
- data/README.md +72 -72
- data/deliver/lib/deliver.rb +1 -0
- data/deliver/lib/deliver/app_screenshot_iterator.rb +95 -0
- data/deliver/lib/deliver/detect_values.rb +4 -1
- data/deliver/lib/deliver/languages.rb +7 -0
- data/deliver/lib/deliver/loader.rb +4 -5
- data/deliver/lib/deliver/queue_worker.rb +64 -0
- data/deliver/lib/deliver/runner.rb +7 -5
- data/deliver/lib/deliver/upload_screenshots.rb +143 -128
- data/fastlane/lib/fastlane/actions/app_store_connect_api_key.rb +120 -0
- data/fastlane/lib/fastlane/actions/commit_version_bump.rb +1 -1
- data/fastlane/lib/fastlane/actions/docs/upload_to_play_store.md +2 -0
- data/fastlane/lib/fastlane/actions/docs/upload_to_testflight.md +17 -1
- data/fastlane/lib/fastlane/actions/set_changelog.rb +2 -2
- data/fastlane/lib/fastlane/actions/sonar.rb +5 -0
- data/fastlane/lib/fastlane/actions/spaceship_stats.rb +73 -0
- data/fastlane/lib/fastlane/actions/upload_to_testflight.rb +4 -0
- data/fastlane/lib/fastlane/version.rb +1 -1
- data/fastlane/swift/Deliverfile.swift +1 -1
- data/fastlane/swift/DeliverfileProtocol.swift +1 -1
- data/fastlane/swift/Fastlane.swift +68 -8
- data/fastlane/swift/Gymfile.swift +1 -1
- data/fastlane/swift/GymfileProtocol.swift +1 -1
- data/fastlane/swift/Matchfile.swift +1 -1
- data/fastlane/swift/MatchfileProtocol.swift +1 -1
- data/fastlane/swift/Precheckfile.swift +1 -1
- data/fastlane/swift/PrecheckfileProtocol.swift +1 -1
- data/fastlane/swift/Scanfile.swift +1 -1
- data/fastlane/swift/ScanfileProtocol.swift +1 -1
- data/fastlane/swift/Screengrabfile.swift +1 -1
- data/fastlane/swift/ScreengrabfileProtocol.swift +1 -1
- data/fastlane/swift/Snapshotfile.swift +1 -1
- data/fastlane/swift/SnapshotfileProtocol.swift +1 -1
- data/fastlane_core/lib/fastlane_core/itunes_transporter.rb +71 -42
- data/fastlane_core/lib/fastlane_core/project.rb +1 -0
- data/gym/lib/gym/error_handler.rb +1 -1
- data/gym/lib/gym/generators/build_command_generator.rb +0 -1
- data/pilot/lib/pilot/build_manager.rb +18 -4
- data/pilot/lib/pilot/manager.rb +15 -5
- data/pilot/lib/pilot/options.rb +16 -0
- data/produce/lib/produce/itunes_connect.rb +2 -2
- data/scan/lib/scan/test_command_generator.rb +3 -1
- data/screengrab/lib/screengrab/runner.rb +8 -7
- data/sigh/lib/sigh/runner.rb +9 -5
- data/snapshot/lib/snapshot/test_command_generator_base.rb +3 -1
- data/spaceship/lib/spaceship.rb +4 -0
- data/spaceship/lib/spaceship/client.rb +2 -0
- data/spaceship/lib/spaceship/connect_api.rb +0 -15
- data/spaceship/lib/spaceship/connect_api/api_client.rb +270 -0
- data/spaceship/lib/spaceship/connect_api/client.rb +139 -213
- data/spaceship/lib/spaceship/connect_api/models/profile.rb +3 -2
- data/spaceship/lib/spaceship/connect_api/provisioning/client.rb +8 -17
- data/spaceship/lib/spaceship/connect_api/provisioning/provisioning.rb +75 -64
- data/spaceship/lib/spaceship/connect_api/spaceship.rb +94 -0
- data/spaceship/lib/spaceship/connect_api/testflight/client.rb +8 -17
- data/spaceship/lib/spaceship/connect_api/testflight/testflight.rb +288 -277
- data/spaceship/lib/spaceship/connect_api/token.rb +46 -5
- data/spaceship/lib/spaceship/connect_api/token_refresh_middleware.rb +24 -0
- data/spaceship/lib/spaceship/connect_api/tunes/client.rb +8 -17
- data/spaceship/lib/spaceship/connect_api/tunes/tunes.rb +717 -706
- data/spaceship/lib/spaceship/connect_api/users/client.rb +8 -17
- data/spaceship/lib/spaceship/connect_api/users/users.rb +28 -17
- data/spaceship/lib/spaceship/stats_middleware.rb +65 -0
- metadata +25 -18
- data/sigh/lib/sigh/.runner.rb.swp +0 -0
- data/spaceship/lib/spaceship/connect_api/models/.device.rb.swp +0 -0
| @@ -320,6 +320,7 @@ module FastlaneCore | |
| 320 320 | 
             
                  proj << "-scheme #{options[:scheme].shellescape}" if options[:scheme]
         | 
| 321 321 | 
             
                  proj << "-project #{options[:project].shellescape}" if options[:project]
         | 
| 322 322 | 
             
                  proj << "-configuration #{options[:configuration].shellescape}" if options[:configuration]
         | 
| 323 | 
            +
                  proj << "-derivedDataPath #{options[:derived_data_path].shellescape}" if options[:derived_data_path]
         | 
| 323 324 | 
             
                  proj << "-xcconfig #{options[:xcconfig].shellescape}" if options[:xcconfig]
         | 
| 324 325 |  | 
| 325 326 | 
             
                  if FastlaneCore::Helper.xcode_at_least?('11.0') && options[:cloned_source_packages_path]
         | 
| @@ -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") | 
| 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
         | 
| @@ -38,7 +38,6 @@ module Gym | |
| 38 38 | 
             
                    options << "-toolchain '#{config[:toolchain]}'" if config[:toolchain]
         | 
| 39 39 | 
             
                    options << "-destination '#{config[:destination]}'" if config[:destination]
         | 
| 40 40 | 
             
                    options << "-archivePath #{archive_path.shellescape}" unless config[:skip_archive]
         | 
| 41 | 
            -
                    options << "-derivedDataPath '#{config[:derived_data_path]}'" if config[:derived_data_path]
         | 
| 42 41 | 
             
                    options << "-resultBundlePath '#{result_bundle_path}'" if config[:result_bundle]
         | 
| 43 42 | 
             
                    options << config[:xcargs] if config[:xcargs]
         | 
| 44 43 | 
             
                    options << "OTHER_SWIFT_FLAGS=\"-Xfrontend -debug-time-function-bodies\"" if config[:analyze_build_time]
         | 
| @@ -343,22 +343,36 @@ 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] ||  | 
| 352 | 
            -
                  return generic_transporter unless  | 
| 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 =  | 
| 365 | 
            +
                    team = tunes_client.teams.find { |t| t['contentProvider']['contentProviderId'].to_s == tunes_client.team_id }
         | 
| 356 366 | 
             
                    name = team['contentProvider']['name']
         | 
| 367 | 
            +
                    STDERR.puts("name: #{name}")
         | 
| 368 | 
            +
                    STDERR.puts("id: #{generic_transporter.provider_ids}")
         | 
| 357 369 | 
             
                    provider_id = generic_transporter.provider_ids[name]
         | 
| 370 | 
            +
                    STDERR.puts("provider_id: #{provider_id}")
         | 
| 358 371 | 
             
                    UI.verbose("Inferred provider id #{provider_id} for team #{name}.")
         | 
| 359 372 | 
             
                    return FastlaneCore::ItunesTransporter.new(options[:username], nil, false, provider_id)
         | 
| 360 373 | 
             
                  rescue => ex
         | 
| 361 | 
            -
                     | 
| 374 | 
            +
                    STDERR.puts(ex.to_s)
         | 
| 375 | 
            +
                    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 376 | 
             
                    return generic_transporter
         | 
| 363 377 | 
             
                  end
         | 
| 364 378 | 
             
                end
         | 
    
        data/pilot/lib/pilot/manager.rb
    CHANGED
    
    | @@ -17,12 +17,22 @@ module Pilot | |
| 17 17 | 
             
                end
         | 
| 18 18 |  | 
| 19 19 | 
             
                def login
         | 
| 20 | 
            -
                   | 
| 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 | 
            -
             | 
| 23 | 
            -
             | 
| 24 | 
            -
             | 
| 25 | 
            -
                   | 
| 26 | 
            +
                    UI.message("Login to App Store Connect (#{config[:username]})")
         | 
| 27 | 
            +
                    Spaceship::ConnectAPI.login(config[:username], 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
         | 
    
        data/pilot/lib/pilot/options.rb
    CHANGED
    
    | @@ -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,8 @@ 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:: | 
| 13 | 
            -
                  Spaceship:: | 
| 12 | 
            +
                  Spaceship::ConnectAPI.login(Produce.config[:username], nil)
         | 
| 13 | 
            +
                  Spaceship::ConnectAPI.client.select_team
         | 
| 14 14 |  | 
| 15 15 | 
             
                  create_new_app
         | 
| 16 16 | 
             
                end
         | 
| @@ -35,7 +35,9 @@ module Scan | |
| 35 35 | 
             
                  options << "-sdk '#{config[:sdk]}'" if config[:sdk]
         | 
| 36 36 | 
             
                  options << destination # generated in `detect_values`
         | 
| 37 37 | 
             
                  options << "-toolchain '#{config[:toolchain]}'" if config[:toolchain]
         | 
| 38 | 
            -
                   | 
| 38 | 
            +
                  if config[:derived_data_path] && !options.include?("-derivedDataPath #{config[:derived_data_path].shellescape}")
         | 
| 39 | 
            +
                    options << "-derivedDataPath #{config[:derived_data_path].shellescape}"
         | 
| 40 | 
            +
                  end
         | 
| 39 41 | 
             
                  options << "-resultBundlePath '#{result_bundle_path}'" if config[:result_bundle]
         | 
| 40 42 | 
             
                  if FastlaneCore::Helper.xcode_at_least?(10)
         | 
| 41 43 | 
             
                    options << "-parallel-testing-worker-count #{config[:concurrent_workers]}" if config[:concurrent_workers]
         | 
| @@ -64,9 +64,10 @@ 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 | 
            -
                  clear_device_previous_screenshots(device_serial, device_screenshots_paths)
         | 
| 70 | 
            +
                  clear_device_previous_screenshots(@config[:app_package_name], device_serial, device_screenshots_paths)
         | 
| 70 71 |  | 
| 71 72 | 
             
                  app_apk_path ||= select_app_apk(discovered_apk_paths)
         | 
| 72 73 | 
             
                  tests_apk_path ||= select_tests_apk(discovered_apk_paths)
         | 
| @@ -157,12 +158,12 @@ module Screengrab | |
| 157 158 | 
             
                  end.flatten
         | 
| 158 159 | 
             
                end
         | 
| 159 160 |  | 
| 160 | 
            -
                def clear_device_previous_screenshots(device_serial, device_screenshots_paths)
         | 
| 161 | 
            +
                def clear_device_previous_screenshots(app_package_name, device_serial, device_screenshots_paths)
         | 
| 161 162 | 
             
                  UI.message('Cleaning screenshots on device')
         | 
| 162 163 |  | 
| 163 164 | 
             
                  device_screenshots_paths.each do |device_path|
         | 
| 164 | 
            -
                    if_device_path_exists(device_serial, device_path) do |path|
         | 
| 165 | 
            -
                      run_adb_command("-s #{device_serial} shell rm -rf #{path}",
         | 
| 165 | 
            +
                    if_device_path_exists(app_package_name, device_serial, device_path) do |path|
         | 
| 166 | 
            +
                      run_adb_command("-s #{device_serial} shell run-as #{app_package_name} rm -rf #{path}",
         | 
| 166 167 | 
             
                                      print_all: true,
         | 
| 167 168 | 
             
                                      print_command: true)
         | 
| 168 169 | 
             
                    end
         | 
| @@ -296,7 +297,7 @@ module Screengrab | |
| 296 297 |  | 
| 297 298 | 
             
                  Dir.mktmpdir do |tempdir|
         | 
| 298 299 | 
             
                    device_screenshots_paths.each do |device_path|
         | 
| 299 | 
            -
                      if_device_path_exists(device_serial, device_path) do |path|
         | 
| 300 | 
            +
                      if_device_path_exists(@config[:app_package_name], device_serial, device_path) do |path|
         | 
| 300 301 | 
             
                        next unless path.include?(locale)
         | 
| 301 302 | 
             
                        run_adb_command("-s #{device_serial} pull #{path} #{tempdir}",
         | 
| 302 303 | 
             
                                        print_all: false,
         | 
| @@ -361,8 +362,8 @@ module Screengrab | |
| 361 362 |  | 
| 362 363 | 
             
                # Some device commands fail if executed against a device path that does not exist, so this helper method
         | 
| 363 364 | 
             
                # provides a way to conditionally execute a block only if the provided path exists on the device.
         | 
| 364 | 
            -
                def if_device_path_exists(device_serial, device_path)
         | 
| 365 | 
            -
                  return if run_adb_command("-s #{device_serial} shell ls #{device_path}",
         | 
| 365 | 
            +
                def if_device_path_exists(app_package_name, device_serial, device_path)
         | 
| 366 | 
            +
                  return if run_adb_command("-s #{device_serial} shell run-as #{app_package_name} ls #{device_path}",
         | 
| 366 367 | 
             
                                            print_all: false,
         | 
| 367 368 | 
             
                                            print_command: false).include?('No such file')
         | 
| 368 369 |  | 
    
        data/sigh/lib/sigh/runner.rb
    CHANGED
    
    | @@ -18,8 +18,8 @@ module Sigh | |
| 18 18 | 
             
                                                         title: "Summary for sigh #{Fastlane::VERSION}")
         | 
| 19 19 |  | 
| 20 20 | 
             
                  UI.message("Starting login with user '#{Sigh.config[:username]}'")
         | 
| 21 | 
            -
                  Spaceship.login(Sigh.config[:username], nil)
         | 
| 22 | 
            -
                  Spaceship.select_team
         | 
| 21 | 
            +
                  Spaceship::ConnectAPI.login(Sigh.config[:username], nil)
         | 
| 22 | 
            +
                  Spaceship::ConnectAPI.select_team
         | 
| 23 23 | 
             
                  UI.message("Successfully logged in")
         | 
| 24 24 |  | 
| 25 25 | 
             
                  profiles = [] if Sigh.config[:skip_fetch_profiles]
         | 
| @@ -60,12 +60,12 @@ module Sigh | |
| 60 60 | 
             
                  case Sigh.config[:platform]
         | 
| 61 61 | 
             
                  when "ios"
         | 
| 62 62 | 
             
                    @profile_type = Spaceship::ConnectAPI::Profile::ProfileType::IOS_APP_STORE
         | 
| 63 | 
            -
                    @profile_type = Spaceship::ConnectAPI::Profile::ProfileType::IOS_APP_INHOUSE if Spaceship.client.in_house?
         | 
| 63 | 
            +
                    @profile_type = Spaceship::ConnectAPI::Profile::ProfileType::IOS_APP_INHOUSE if Spaceship::ConnectAPI.client.in_house?
         | 
| 64 64 | 
             
                    @profile_type = Spaceship::ConnectAPI::Profile::ProfileType::IOS_APP_ADHOC if Sigh.config[:adhoc]
         | 
| 65 65 | 
             
                    @profile_type = Spaceship::ConnectAPI::Profile::ProfileType::IOS_APP_DEVELOPMENT if Sigh.config[:development]
         | 
| 66 66 | 
             
                  when "tvos"
         | 
| 67 67 | 
             
                    @profile_type = Spaceship::ConnectAPI::Profile::ProfileType::TVOS_APP_STORE
         | 
| 68 | 
            -
                    @profile_type = Spaceship::ConnectAPI::Profile::ProfileType::TVOS_APP_INHOUSE if Spaceship.client.in_house?
         | 
| 68 | 
            +
                    @profile_type = Spaceship::ConnectAPI::Profile::ProfileType::TVOS_APP_INHOUSE if Spaceship::ConnectAPI.client.in_house?
         | 
| 69 69 | 
             
                    @profile_type = Spaceship::ConnectAPI::Profile::ProfileType::TVOS_APP_ADHOC if Sigh.config[:adhoc]
         | 
| 70 70 | 
             
                    @profile_type = Spaceship::ConnectAPI::Profile::ProfileType::TVOS_APP_DEVELOPMENT if Sigh.config[:development]
         | 
| 71 71 | 
             
                  when "macos"
         | 
| @@ -172,7 +172,8 @@ module Sigh | |
| 172 172 | 
             
                    profile_type: profile_type,
         | 
| 173 173 | 
             
                    bundle_id_id: bundle_id.id,
         | 
| 174 174 | 
             
                    certificate_ids: certificates_to_use.map(&:id),
         | 
| 175 | 
            -
                    device_ids: devices_to_use.map(&:id)
         | 
| 175 | 
            +
                    device_ids: devices_to_use.map(&:id),
         | 
| 176 | 
            +
                    template_name: Sigh.config[:template_name]
         | 
| 176 177 | 
             
                  )
         | 
| 177 178 |  | 
| 178 179 | 
             
                  profile
         | 
| @@ -228,6 +229,9 @@ module Sigh | |
| 228 229 | 
             
                end
         | 
| 229 230 |  | 
| 230 231 | 
             
                def devices_to_use
         | 
| 232 | 
            +
                  # Only use devices if development or adhoc
         | 
| 233 | 
            +
                  return [] if !Sigh.config[:development] && !Sigh.config[:adhoc]
         | 
| 234 | 
            +
             | 
| 231 235 | 
             
                  device_class = case Sigh.config[:platform].to_s
         | 
| 232 236 | 
             
                                 when 'ios'
         | 
| 233 237 | 
             
                                   [
         | 
| @@ -24,7 +24,9 @@ module Snapshot | |
| 24 24 | 
             
                    options = []
         | 
| 25 25 | 
             
                    options += project_path_array
         | 
| 26 26 | 
             
                    options << "-sdk '#{config[:sdk]}'" if config[:sdk]
         | 
| 27 | 
            -
                     | 
| 27 | 
            +
                    if derived_data_path && !options.include?("-derivedDataPath #{derived_data_path.shellescape}")
         | 
| 28 | 
            +
                      options << "-derivedDataPath #{derived_data_path.shellescape}"
         | 
| 29 | 
            +
                    end
         | 
| 28 30 | 
             
                    options << "-resultBundlePath '#{result_bundle_path}'" if result_bundle_path
         | 
| 29 31 | 
             
                    if FastlaneCore::Helper.xcode_at_least?(11)
         | 
| 30 32 | 
             
                      options << "-testPlan '#{config[:testplan]}'" if config[:testplan]
         | 
    
        data/spaceship/lib/spaceship.rb
    CHANGED
    
    | @@ -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'
         | 
| @@ -16,6 +16,7 @@ require_relative 'errors' | |
| 16 16 | 
             
            require_relative 'tunes/errors'
         | 
| 17 17 | 
             
            require_relative 'globals'
         | 
| 18 18 | 
             
            require_relative 'provider'
         | 
| 19 | 
            +
            require_relative 'stats_middleware'
         | 
| 19 20 |  | 
| 20 21 | 
             
            Faraday::Utils.default_params_encoder = Faraday::FlatParamsEncoder
         | 
| 21 22 |  | 
| @@ -209,6 +210,7 @@ module Spaceship | |
| 209 210 | 
             
                    c.response(:plist, content_type: /\bplist$/)
         | 
| 210 211 | 
             
                    c.use(:cookie_jar, jar: @cookie)
         | 
| 211 212 | 
             
                    c.use(FaradayMiddleware::RelsMiddleware)
         | 
| 213 | 
            +
                    c.use(Spaceship::StatsMiddleware)
         | 
| 212 214 | 
             
                    c.adapter(Faraday.default_adapter)
         | 
| 213 215 |  | 
| 214 216 | 
             
                    if ENV['SPACESHIP_DEBUG']
         | 
| @@ -56,21 +56,6 @@ require 'spaceship/connect_api/models/territory' | |
| 56 56 |  | 
| 57 57 | 
             
            module Spaceship
         | 
| 58 58 | 
             
              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 59 | 
             
                # Defined in the App Store Connect API docs:
         | 
| 75 60 | 
             
                # https://developer.apple.com/documentation/appstoreconnectapi/platform
         | 
| 76 61 | 
             
                #
         | 
| @@ -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
         |