fastlane 2.156.1 → 2.157.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +80 -80
- data/deliver/lib/deliver.rb +1 -0
- data/deliver/lib/deliver/app_screenshot_iterator.rb +26 -29
- 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/runner.rb +7 -5
- data/deliver/lib/deliver/upload_screenshots.rb +34 -17
- 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/gym/lib/gym/error_handler.rb +1 -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/screengrab/lib/screengrab/runner.rb +1 -0
- data/sigh/lib/sigh/runner.rb +4 -4
- 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/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 -19
- data/spaceship/lib/spaceship/connect_api/.client.rb.swp +0 -0
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
|
@@ -1,245 +1,171 @@
|
|
1
|
-
require_relative '
|
2
|
-
require_relative './
|
1
|
+
require_relative './token'
|
2
|
+
require_relative './provisioning/provisioning'
|
3
|
+
require_relative './testflight/testflight'
|
4
|
+
require_relative './tunes/tunes'
|
5
|
+
require_relative './users/users'
|
3
6
|
|
4
7
|
module Spaceship
|
5
8
|
class ConnectAPI
|
6
|
-
class Client
|
9
|
+
class Client
|
7
10
|
attr_accessor :token
|
11
|
+
attr_accessor :tunes_client
|
12
|
+
attr_accessor :portal_client
|
8
13
|
|
9
|
-
|
10
|
-
# @!group Client Init
|
11
|
-
#####################################################
|
12
|
-
|
13
|
-
# Instantiates a client with cookie session or a JWT token.
|
14
|
-
def initialize(cookie: nil, current_team_id: nil, token: nil)
|
15
|
-
if token.nil?
|
16
|
-
super(cookie: cookie, current_team_id: current_team_id, timeout: 1200)
|
17
|
-
else
|
18
|
-
options = {
|
19
|
-
request: {
|
20
|
-
timeout: (ENV["SPACESHIP_TIMEOUT"] || 300).to_i,
|
21
|
-
open_timeout: (ENV["SPACESHIP_TIMEOUT"] || 300).to_i
|
22
|
-
}
|
23
|
-
}
|
24
|
-
@token = token
|
25
|
-
@current_team_id = current_team_id
|
26
|
-
|
27
|
-
hostname = "https://api.appstoreconnect.apple.com/v1/"
|
28
|
-
|
29
|
-
@client = Faraday.new(hostname, options) do |c|
|
30
|
-
c.response(:json, content_type: /\bjson$/)
|
31
|
-
c.response(:plist, content_type: /\bplist$/)
|
32
|
-
c.use(FaradayMiddleware::RelsMiddleware)
|
33
|
-
c.adapter(Faraday.default_adapter)
|
34
|
-
c.headers["Authorization"] = "Bearer #{token.text}"
|
35
|
-
|
36
|
-
if ENV['SPACESHIP_DEBUG']
|
37
|
-
# for debugging only
|
38
|
-
# This enables tracking of networking requests using Charles Web Proxy
|
39
|
-
c.proxy = "https://127.0.0.1:8888"
|
40
|
-
c.ssl[:verify_mode] = OpenSSL::SSL::VERIFY_NONE
|
41
|
-
elsif ENV["SPACESHIP_PROXY"]
|
42
|
-
c.proxy = ENV["SPACESHIP_PROXY"]
|
43
|
-
c.ssl[:verify_mode] = OpenSSL::SSL::VERIFY_NONE if ENV["SPACESHIP_PROXY_SSL_VERIFY_NONE"]
|
44
|
-
end
|
45
|
-
|
46
|
-
if ENV["DEBUG"]
|
47
|
-
puts("To run spaceship through a local proxy, use SPACESHIP_DEBUG")
|
48
|
-
end
|
49
|
-
end
|
50
|
-
end
|
51
|
-
end
|
52
|
-
|
53
|
-
def self.hostname
|
54
|
-
return nil
|
55
|
-
end
|
56
|
-
|
14
|
+
# Initializes client with Apple's App Store Connect JWT auth key.
|
57
15
|
#
|
58
|
-
#
|
16
|
+
# This method will automatically use the key id, issuer id, and filepath from environment
|
17
|
+
# variables if not given.
|
59
18
|
#
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
params[:limit] = limit if limit
|
73
|
-
params[:sort] = sort if sort
|
74
|
-
params[:cursor] = cursor if cursor
|
75
|
-
|
76
|
-
return params
|
19
|
+
# All three parameters are needed to authenticate.
|
20
|
+
#
|
21
|
+
# @param key_id (String) (optional): The key id
|
22
|
+
# @param issuer_id (String) (optional): The issuer id
|
23
|
+
# @param filepath (String) (optional): The filepath
|
24
|
+
#
|
25
|
+
# @raise InvalidUserCredentialsError: raised if authentication failed
|
26
|
+
#
|
27
|
+
# @return (Spaceship::ConnectAPI::Client) The client the login method was called for
|
28
|
+
def self.auth(key_id: nil, issuer_id: nil, filepath: nil)
|
29
|
+
token = Spaceship::ConnectAPI::Token.create(key_id: key_id, issuer_id: issuer_id, filepath: filepath)
|
30
|
+
return ConnectAPI::Client.new(token: token)
|
77
31
|
end
|
78
32
|
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
33
|
+
# Authenticates with Apple's web services. This method has to be called once
|
34
|
+
# to generate a valid session.
|
35
|
+
#
|
36
|
+
# This method will automatically use the username from the Appfile (if available)
|
37
|
+
# and fetch the password from the Keychain (if available)
|
38
|
+
#
|
39
|
+
# @param user (String) (optional): The username (usually the email address)
|
40
|
+
# @param password (String) (optional): The password
|
41
|
+
# @param team_id (String) (optional): The team id
|
42
|
+
# @param team_name (String) (optional): The team name
|
43
|
+
#
|
44
|
+
# @raise InvalidUserCredentialsError: raised if authentication failed
|
45
|
+
#
|
46
|
+
# @return (Spaceship::ConnectAPI::Client) The client the login method was called for
|
47
|
+
def self.login(user = nil, password = nil, team_id: nil, team_name: nil)
|
48
|
+
tunes_client = TunesClient.login(user, password)
|
49
|
+
portal_client = PortalClient.login(user, password)
|
50
|
+
|
51
|
+
# The clients will automatically select the first team if none is given
|
52
|
+
if !team_id.nil? || !team_name.nil?
|
53
|
+
tunes_client.select_team(team_id: team_id, team_name: team_name)
|
54
|
+
portal_client.select_team(team_id: team_id, team_name: team_name)
|
87
55
|
end
|
88
|
-
handle_response(response)
|
89
|
-
end
|
90
56
|
|
91
|
-
|
92
|
-
response = with_asc_retry(tries) do
|
93
|
-
request(:post) do |req|
|
94
|
-
req.url(url_or_path)
|
95
|
-
req.body = body.to_json
|
96
|
-
req.headers['Content-Type'] = 'application/json'
|
97
|
-
end
|
98
|
-
end
|
99
|
-
handle_response(response)
|
57
|
+
return ConnectAPI::Client.new(tunes_client: tunes_client, portal_client: portal_client)
|
100
58
|
end
|
101
59
|
|
102
|
-
def
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
60
|
+
def initialize(cookie: nil, current_team_id: nil, token: nil, tunes_client: nil, portal_client: nil)
|
61
|
+
@token = token
|
62
|
+
|
63
|
+
# If using web session...
|
64
|
+
# Spaceship::Tunes is needed for TestFlight::API, Tunes::API, and Users::API
|
65
|
+
# Spaceship::Portal is needed for Provisioning::API
|
66
|
+
@tunes_client = tunes_client
|
67
|
+
@portal_client = portal_client
|
68
|
+
|
69
|
+
# Extending this instance to add API endpoints from these modules
|
70
|
+
# Each of these modules adds a new setter method for an instance
|
71
|
+
# of an ConnectAPI::APIClient
|
72
|
+
# These get set in set_indvidual_clients
|
73
|
+
self.extend(Spaceship::ConnectAPI::TestFlight::API)
|
74
|
+
self.extend(Spaceship::ConnectAPI::Tunes::API)
|
75
|
+
self.extend(Spaceship::ConnectAPI::Provisioning::API)
|
76
|
+
self.extend(Spaceship::ConnectAPI::Users::API)
|
77
|
+
|
78
|
+
set_indvidual_clients(
|
79
|
+
cookie: cookie,
|
80
|
+
current_team_id: current_team_id,
|
81
|
+
token: token,
|
82
|
+
tunes_client: @tunes_client,
|
83
|
+
portal_client: @portal_client
|
84
|
+
)
|
111
85
|
end
|
112
86
|
|
113
|
-
def
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
87
|
+
def in_house?
|
88
|
+
if token
|
89
|
+
if token.in_house.nil?
|
90
|
+
message = [
|
91
|
+
"Cannot determine if team is App Store or Enterprise via the App Store Connect API (yet)",
|
92
|
+
"Set 'in_house' on your Spaceship::ConnectAPI::Token",
|
93
|
+
"Or set 'in_house' in your App Store Connect API key JSON file",
|
94
|
+
"Or set the 'SPACESHIP_CONNECT_API_IN_HOUSE' environment variable to 'true'",
|
95
|
+
"View more info in the docs at https://docs.fastlane.tools/app-store-connect-api/"
|
96
|
+
]
|
97
|
+
raise message.join('\n')
|
121
98
|
end
|
99
|
+
return !!token.in_house
|
100
|
+
elsif @portal_client
|
101
|
+
return @portal_client.in_house?
|
102
|
+
else
|
103
|
+
raise "No App Store Connect API token or Portal Client set"
|
122
104
|
end
|
123
|
-
handle_response(response)
|
124
105
|
end
|
125
106
|
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
107
|
+
def select_team(team_id: nil, team_name: nil)
|
108
|
+
@tunes_client.select_team(team_id: team_id, team_name: team_name)
|
109
|
+
@portal_client.select_team(team_id: team_id, team_name: team_name)
|
110
|
+
|
111
|
+
# Updating the tunes and portal clients requires resetting
|
112
|
+
# of the clients in the API modules
|
113
|
+
set_indvidual_clients(
|
114
|
+
cookie: nil,
|
115
|
+
current_team_id: nil,
|
116
|
+
token: nil,
|
117
|
+
tunes_client: tunes_client,
|
118
|
+
portal_client: portal_client
|
119
|
+
)
|
120
|
+
end
|
131
121
|
|
132
|
-
|
122
|
+
private
|
133
123
|
|
134
|
-
|
135
|
-
|
136
|
-
|
124
|
+
def set_indvidual_clients(cookie: nil, current_team_id: nil, token: nil, tunes_client: nil, portal_client: nil)
|
125
|
+
# This was added by Spaceship::ConnectAPI::TestFlight::API and is required
|
126
|
+
# to be set for API methods to have a client to send request on
|
127
|
+
if cookie || token || tunes_client
|
128
|
+
self.test_flight_request_client = Spaceship::ConnectAPI::TestFlight::Client.new(
|
129
|
+
cookie: cookie,
|
130
|
+
current_team_id: current_team_id,
|
131
|
+
token: token,
|
132
|
+
another_client: tunes_client
|
133
|
+
)
|
137
134
|
end
|
138
135
|
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
136
|
+
# This was added by Spaceship::ConnectAPI::Tunes::API and is required
|
137
|
+
# to be set for API methods to have a client to send request on
|
138
|
+
if cookie || token || tunes_client
|
139
|
+
self.tunes_request_client = Spaceship::ConnectAPI::Tunes::Client.new(
|
140
|
+
cookie: cookie,
|
141
|
+
current_team_id: current_team_id,
|
142
|
+
token: token,
|
143
|
+
another_client: tunes_client
|
144
|
+
)
|
147
145
|
end
|
148
|
-
end
|
149
146
|
|
150
|
-
|
151
|
-
|
152
|
-
|
147
|
+
# This was added by Spaceship::ConnectAPI::Provisioning::API and is required
|
148
|
+
# to be set for API methods to have a client to send request on
|
149
|
+
if cookie || token || portal_client
|
150
|
+
self.provisioning_request_client = Spaceship::ConnectAPI::Provisioning::Client.new(
|
151
|
+
cookie: cookie,
|
152
|
+
current_team_id: current_team_id,
|
153
|
+
token: token,
|
154
|
+
another_client: portal_client
|
155
|
+
)
|
153
156
|
end
|
154
157
|
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
158
|
+
# This was added by Spaceship::ConnectAPI::Users::API and is required
|
159
|
+
# to be set for API methods to have a client to send request on
|
160
|
+
if cookie || token || tunes_client
|
161
|
+
self.users_request_client = Spaceship::ConnectAPI::Users::Client.new(
|
162
|
+
cookie: cookie,
|
163
|
+
current_team_id: current_team_id,
|
164
|
+
token: token,
|
165
|
+
another_client: tunes_client
|
166
|
+
)
|
159
167
|
end
|
160
|
-
|
161
|
-
raise UnexpectedResponse, response.body['error'] if response.body['error']
|
162
|
-
|
163
|
-
raise UnexpectedResponse, handle_errors(response) if response.body['errors']
|
164
|
-
|
165
|
-
raise UnexpectedResponse, "Temporary App Store Connect error: #{response.body}" if response.body['statusCode'] == 'ERROR'
|
166
|
-
|
167
|
-
store_csrf_tokens(response)
|
168
|
-
|
169
|
-
return Spaceship::ConnectAPI::Response.new(body: response.body, status: response.status, client: self)
|
170
|
-
end
|
171
|
-
|
172
|
-
def handle_errors(response)
|
173
|
-
# Example error format
|
174
|
-
# {
|
175
|
-
# "errors":[
|
176
|
-
# {
|
177
|
-
# "id":"cbfd8674-4802-4857-bfe8-444e1ea36e32",
|
178
|
-
# "status":"409",
|
179
|
-
# "code":"STATE_ERROR",
|
180
|
-
# "title":"The request cannot be fulfilled because of the state of another resource.",
|
181
|
-
# "detail":"Submit for review errors found.",
|
182
|
-
# "meta":{
|
183
|
-
# "associatedErrors":{
|
184
|
-
# "/v1/appScreenshots/":[
|
185
|
-
# {
|
186
|
-
# "id":"23d1734f-b81f-411a-98e4-6d3e763d54ed",
|
187
|
-
# "status":"409",
|
188
|
-
# "code":"STATE_ERROR.SCREENSHOT_REQUIRED.APP_WATCH_SERIES_4",
|
189
|
-
# "title":"App screenshot missing (APP_WATCH_SERIES_4)."
|
190
|
-
# },
|
191
|
-
# {
|
192
|
-
# "id":"db993030-0a93-48e9-9fd7-7e5676633431",
|
193
|
-
# "status":"409",
|
194
|
-
# "code":"STATE_ERROR.SCREENSHOT_REQUIRED.APP_WATCH_SERIES_4",
|
195
|
-
# "title":"App screenshot missing (APP_WATCH_SERIES_4)."
|
196
|
-
# }
|
197
|
-
# ],
|
198
|
-
# "/v1/builds/d710b6fa-5235-4fe4-b791-2b80d6818db0":[
|
199
|
-
# {
|
200
|
-
# "id":"e421fe6f-0e3b-464b-89dc-ba437e7bb77d",
|
201
|
-
# "status":"409",
|
202
|
-
# "code":"ENTITY_ERROR.ATTRIBUTE.REQUIRED",
|
203
|
-
# "title":"The provided entity is missing a required attribute",
|
204
|
-
# "detail":"You must provide a value for the attribute 'usesNonExemptEncryption' with this request",
|
205
|
-
# "source":{
|
206
|
-
# "pointer":"/data/attributes/usesNonExemptEncryption"
|
207
|
-
# }
|
208
|
-
# }
|
209
|
-
# ]
|
210
|
-
# }
|
211
|
-
# }
|
212
|
-
# }
|
213
|
-
# ]
|
214
|
-
# }
|
215
|
-
|
216
|
-
return response.body['errors'].map do |error|
|
217
|
-
messages = [[error['title'], error['detail']].compact.join(" - ")]
|
218
|
-
|
219
|
-
meta = error["meta"] || {}
|
220
|
-
associated_errors = meta["associatedErrors"] || {}
|
221
|
-
|
222
|
-
messages + associated_errors.values.flatten.map do |associated_error|
|
223
|
-
[[associated_error["title"], associated_error["detail"]].compact.join(" - ")]
|
224
|
-
end
|
225
|
-
end.flatten.join("\n")
|
226
|
-
end
|
227
|
-
|
228
|
-
private
|
229
|
-
|
230
|
-
def local_variable_get(binding, name)
|
231
|
-
if binding.respond_to?(:local_variable_get)
|
232
|
-
binding.local_variable_get(name)
|
233
|
-
else
|
234
|
-
binding.eval(name.to_s)
|
235
|
-
end
|
236
|
-
end
|
237
|
-
|
238
|
-
def provider_id
|
239
|
-
return team_id if self.provider.nil?
|
240
|
-
self.provider.provider_id
|
241
168
|
end
|
242
169
|
end
|
243
170
|
end
|
244
|
-
# rubocop:enable Metrics/ClassLength
|
245
171
|
end
|