fastlane 2.155.3 → 2.157.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +78 -78
- 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 +67 -7
- 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/command_executor.rb +1 -0
- 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 +16 -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 +36 -17
- data/sigh/lib/sigh/runner.rb +4 -4
- 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 +144 -213
- 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 +98 -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 +26 -23
- data/match/lib/match/.options.rb.swp +0 -0
- data/match/lib/match/.runner.rb.swp +0 -0
- data/sigh/lib/sigh/.options.rb.swp +0 -0
- data/sigh/lib/sigh/.runner.rb.swp +0 -0
- data/spaceship/lib/spaceship/connect_api/models/.profile.rb.swp +0 -0
- data/spaceship/lib/spaceship/connect_api/provisioning/.provisioning.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,23 @@ 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], use_portal: false, use_tunes: true)
|
28
|
+
Spaceship::ConnectAPI.select_team(tunes_team_id: config[:team_id], team_name: config[:team_name])
|
29
|
+
UI.message("Login successful")
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def api_token
|
34
|
+
@api_token ||= Spaceship::ConnectAPI::Token.create(config[:api_key]) if config[:api_key]
|
35
|
+
@api_token ||= Spaceship::ConnectAPI::Token.from_json_file(config[:api_key_path]) if config[:api_key_path]
|
36
|
+
return @api_token
|
26
37
|
end
|
27
38
|
|
28
39
|
# 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, use_portal: false, use_tunes: true)
|
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)
|
@@ -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?
|
@@ -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,11 +297,19 @@ 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
|
-
run_adb_command("-s #{device_serial} pull #{path} #{tempdir}",
|
302
|
-
|
303
|
-
|
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
|
|
@@ -361,8 +370,8 @@ module Screengrab
|
|
361
370
|
|
362
371
|
# Some device commands fail if executed against a device path that does not exist, so this helper method
|
363
372
|
# 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}",
|
373
|
+
def if_device_path_exists(app_package_name, device_serial, device_path)
|
374
|
+
return if run_adb_command("-s #{device_serial} shell run-as #{app_package_name} ls #{device_path}",
|
366
375
|
print_all: false,
|
367
376
|
print_command: false).include?('No such file')
|
368
377
|
|
@@ -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 =
|
388
|
-
|
389
|
-
|
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
|
|
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, use_portal: true, use_tunes: false)
|
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"
|
@@ -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
|