spaceship 0.24.1 → 0.25.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 +18 -0
- data/bin/spaceship +2 -2
- data/lib/spaceship/client.rb +101 -0
- data/lib/spaceship/du/du_client.rb +1 -1
- data/lib/spaceship/du/upload_file.rb +4 -1
- data/lib/spaceship/du/utilities.rb +6 -1
- data/lib/spaceship/portal/portal_client.rb +11 -23
- data/lib/spaceship/tunes/app_version.rb +13 -1
- data/lib/spaceship/tunes/app_version_generated_promocodes.rb +35 -0
- data/lib/spaceship/tunes/app_version_promocodes.rb +34 -0
- data/lib/spaceship/tunes/application.rb +17 -0
- data/lib/spaceship/tunes/build.rb +4 -21
- data/lib/spaceship/tunes/recovery_device.rb +69 -0
- data/lib/spaceship/tunes/tunes.rb +4 -0
- data/lib/spaceship/tunes/tunes_client.rb +39 -50
- data/lib/spaceship/two_step_client.rb +155 -0
- data/lib/spaceship/version.rb +1 -1
- metadata +49 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c8b9baae7a579a3b8791d93715b93f4ab301bdbb
|
4
|
+
data.tar.gz: 846ed6e24a001c17d75a5fb7ec6c551a697927ee
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b56a873a114281b421cc7032582e203097e50e41406699510a4593c941db4cc8e42c77942a402e6acff9a67ab9cea08094ad565931543123989d56d5fa699e44
|
7
|
+
data.tar.gz: db26d7aa79f1ea99432bc45d2c142eddc7a86bc43404fe858a068a5f7296b1094333ac058710b3806e5e29978375732cfb412b10f8507c3f13ec37be42390ddd
|
data/README.md
CHANGED
@@ -112,6 +112,24 @@ This requires you to install `pry` using `sudo gem install pry`. `pry` is not in
|
|
112
112
|
|
113
113
|
##### Open [iTunesConnect.md](docs/iTunesConnect.md) for code samples
|
114
114
|
|
115
|
+
## 2 Step Verification
|
116
|
+
|
117
|
+
When your Apple account has 2 step verification enabled, you'll automatically be asked to verify your identity using your phone. The resulting session will be stored in `~/.spaceship/[email]/cookie`. The session should be valid for about one month, however there is no way to test this without actually waiting for over a month.
|
118
|
+
|
119
|
+
Since your CI system probably doesn't allow you to input values (like the verification code), you can use `spaceauth`:
|
120
|
+
|
121
|
+
```sh
|
122
|
+
spaceauth -u apple@krausefx.com
|
123
|
+
```
|
124
|
+
|
125
|
+
This will authenticate you and provide a string that can be transfered to your CI system:
|
126
|
+
|
127
|
+
```
|
128
|
+
export FASTLANE_SESSION='---\n- !ruby/object:HTTP::Cookie\n name: DES5c148586dfd451e55afbaaa5f62418f91\n value: HSARMTKNSRVTWFla1+yO4gVPowH17VaaaxPFnUdMUegQZxqy1Ie1c2v6bM1vSOzIbuOmrl/FNenlScsd/NbF7/Lw4cpnL15jsyg0TOJwP32tC/NguPiyOaaaU+jrj4tf4uKdIywVaaaFSRVT\n domain: idmsa.apple.com\n for_domain: true\n path: "/"\n secure: true\n httponly: true\n expires: 2016-04-27 23:55:56.000000000 Z\n max_age: \n created_at: 2016-03-28 16:55:57.032086000 -07:00\n accessed_at: 2016-03-28 19:11:17.828141000 -07:00\n'
|
129
|
+
```
|
130
|
+
|
131
|
+
Copy everything from `---\n` to your CI server and provide it as environment variable named `FASTLANE_SESSION`.
|
132
|
+
|
115
133
|
### Spaceship in use
|
116
134
|
|
117
135
|
Most [fastlane tools](https://fastlane.tools) already use `spaceship`, like `sigh`, `cert`, `produce`, `pilot` and `boarding`.
|
data/bin/spaceship
CHANGED
@@ -42,7 +42,7 @@ begin
|
|
42
42
|
puts "Successfully logged in to iTunes Connect".green
|
43
43
|
puts ""
|
44
44
|
rescue
|
45
|
-
puts "Could not login to iTunes Connect..."
|
45
|
+
puts "Could not login to iTunes Connect...".red
|
46
46
|
end
|
47
47
|
begin
|
48
48
|
puts "Logging into the Developer Portal (#{username})..."
|
@@ -50,7 +50,7 @@ begin
|
|
50
50
|
puts "Successfully logged in to the Developer Portal".green
|
51
51
|
puts ""
|
52
52
|
rescue
|
53
|
-
puts "Could not login to the Developer Portal..."
|
53
|
+
puts "Could not login to the Developer Portal...".red
|
54
54
|
end
|
55
55
|
|
56
56
|
puts "---------------------------------------".green
|
data/lib/spaceship/client.rb
CHANGED
@@ -148,6 +148,21 @@ module Spaceship
|
|
148
148
|
@cookie.map(&:to_s).join(';')
|
149
149
|
end
|
150
150
|
|
151
|
+
def store_cookie(path: nil)
|
152
|
+
path ||= persistent_cookie_path
|
153
|
+
|
154
|
+
# really important to specify the session to true
|
155
|
+
# otherwise myacinfo and more won't be stored
|
156
|
+
@cookie.save(path, :yaml, session: true)
|
157
|
+
return File.read(path)
|
158
|
+
end
|
159
|
+
|
160
|
+
def persistent_cookie_path
|
161
|
+
path = File.expand_path(File.join("~", ".spaceship", self.user, "cookie"))
|
162
|
+
FileUtils.mkdir_p(File.expand_path("..", path))
|
163
|
+
return path
|
164
|
+
end
|
165
|
+
|
151
166
|
#####################################################
|
152
167
|
# @!group Automatic Paging
|
153
168
|
#####################################################
|
@@ -219,6 +234,90 @@ module Spaceship
|
|
219
234
|
end
|
220
235
|
end
|
221
236
|
|
237
|
+
# This method is used for both the Apple Dev Portal and iTunes Connect
|
238
|
+
# This will also handle 2 step verification
|
239
|
+
def send_shared_login_request(user, password)
|
240
|
+
# First we see if we have a stored cookie for 2 step enabled accounts
|
241
|
+
# this is needed as it stores the information on if this computer is a
|
242
|
+
# trusted one. In general I think spaceship clients should be trusted
|
243
|
+
load_session_from_file
|
244
|
+
# If this is a CI, the user can pass the session via environment variable
|
245
|
+
load_session_from_env
|
246
|
+
|
247
|
+
data = {
|
248
|
+
accountName: user,
|
249
|
+
password: password,
|
250
|
+
rememberMe: true
|
251
|
+
}
|
252
|
+
|
253
|
+
begin
|
254
|
+
# The below workaround is only needed for 2 step verified machines
|
255
|
+
# Due to escaping of cookie values we have a little workaround here
|
256
|
+
# By default the cookie jar would generate the following header
|
257
|
+
# DES5c148...=HSARM.......xaA/O69Ws/CHfQ==SRVT
|
258
|
+
# However we need the following
|
259
|
+
# DES5c148...="HSARM.......xaA/O69Ws/CHfQ==SRVT"
|
260
|
+
# There is no way to get the cookie jar value with " around the value
|
261
|
+
# so we manually modify the cookie (only this one) to be properly escaped
|
262
|
+
# Afterwards we pass this value manually as a header
|
263
|
+
# It's not enough to just modify @cookie, it needs to be done after self.cookie
|
264
|
+
# as a string operation
|
265
|
+
important_cookie = @cookie.store.entries.find { |a| a.name.include?("DES") }
|
266
|
+
if important_cookie
|
267
|
+
modified_cookie = self.cookie # returns a string of all cookies
|
268
|
+
unescaped_important_cookie = "#{important_cookie.name}=#{important_cookie.value}"
|
269
|
+
escaped_important_cookie = "#{important_cookie.name}=\"#{important_cookie.value}\""
|
270
|
+
modified_cookie.gsub!(unescaped_important_cookie, escaped_important_cookie)
|
271
|
+
end
|
272
|
+
|
273
|
+
response = request(:post) do |req|
|
274
|
+
req.url "https://idmsa.apple.com/appleauth/auth/signin?widgetKey=#{itc_service_key}"
|
275
|
+
req.body = data.to_json
|
276
|
+
req.headers['Content-Type'] = 'application/json'
|
277
|
+
req.headers['X-Requested-With'] = 'XMLHttpRequest'
|
278
|
+
req.headers['Accept'] = 'application/json, text/javascript'
|
279
|
+
req.headers["Cookie"] = modified_cookie if modified_cookie
|
280
|
+
end
|
281
|
+
rescue UnauthorizedAccessError
|
282
|
+
raise InvalidUserCredentialsError.new, "Invalid username and password combination. Used '#{user}' as the username."
|
283
|
+
end
|
284
|
+
|
285
|
+
# get woinst, wois, and itctx cookie values
|
286
|
+
request(:get, "https://itunesconnect.apple.com/WebObjects/iTunesConnect.woa/wa/route?noext")
|
287
|
+
request(:get, "https://itunesconnect.apple.com/WebObjects/iTunesConnect.woa")
|
288
|
+
|
289
|
+
case response.status
|
290
|
+
when 403
|
291
|
+
raise InvalidUserCredentialsError.new, "Invalid username and password combination. Used '#{user}' as the username."
|
292
|
+
when 200
|
293
|
+
return response
|
294
|
+
else
|
295
|
+
if response["Location"] == "/auth" # redirect to 2 step auth page
|
296
|
+
handle_two_step(response)
|
297
|
+
return true
|
298
|
+
elsif (response.body || "").include?('invalid="true"')
|
299
|
+
# User Credentials are wrong
|
300
|
+
raise InvalidUserCredentialsError.new, "Invalid username and password combination. Used '#{user}' as the username."
|
301
|
+
elsif (response['Set-Cookie'] || "").include?("itctx")
|
302
|
+
raise "Looks like your Apple ID is not enabled for iTunes Connect, make sure to be able to login online"
|
303
|
+
else
|
304
|
+
info = [response.body, response['Set-Cookie']]
|
305
|
+
raise ITunesConnectError.new, info.join("\n")
|
306
|
+
end
|
307
|
+
end
|
308
|
+
end
|
309
|
+
|
310
|
+
def itc_service_key
|
311
|
+
return @service_key if @service_key
|
312
|
+
# We need a service key from a JS file to properly auth
|
313
|
+
js = request(:get, "https://itunesconnect.apple.com/itc/static-resources/controllers/login_cntrl.js")
|
314
|
+
@service_key ||= js.body.match(/itcServiceKey = '(.*)'/)[1]
|
315
|
+
end
|
316
|
+
|
317
|
+
#####################################################
|
318
|
+
# @!group Helpers
|
319
|
+
#####################################################
|
320
|
+
|
222
321
|
def with_retry(tries = 5, &_block)
|
223
322
|
return yield
|
224
323
|
rescue Faraday::Error::ConnectionFailed, Faraday::Error::TimeoutError, AppleTimeoutError, Errno::EPIPE => ex # New Faraday version: Faraday::TimeoutError => ex
|
@@ -339,3 +438,5 @@ module Spaceship
|
|
339
438
|
end
|
340
439
|
end
|
341
440
|
end
|
441
|
+
|
442
|
+
require 'spaceship/two_step_client'
|
@@ -34,7 +34,7 @@ module Spaceship
|
|
34
34
|
upload_file(app_version, upload_file, '/upload/purple-video', content_provider_id, sso_token_for_video)
|
35
35
|
end
|
36
36
|
|
37
|
-
def
|
37
|
+
def upload_trailer_preview(app_version, upload_file, content_provider_id, sso_token_for_image)
|
38
38
|
upload_file(app_version, upload_file, '/upload/app-screenshot-image', content_provider_id, sso_token_for_image)
|
39
39
|
end
|
40
40
|
|
@@ -12,12 +12,15 @@ module Spaceship
|
|
12
12
|
class << self
|
13
13
|
def from_path(path)
|
14
14
|
raise "Image must exists at path: #{path}" unless File.exist?(path)
|
15
|
+
|
16
|
+
# md5 from original. keeping track of md5s allows to skip previously uploaded in deliver
|
17
|
+
content_md5 = Spaceship::Utilities.md5digest(path)
|
15
18
|
path = remove_alpha_channel(path) if File.extname(path).casecmp('.png').zero?
|
16
19
|
|
17
20
|
content_type = Utilities.content_type(path)
|
18
21
|
self.new(
|
19
22
|
file_path: path,
|
20
|
-
file_name: File.basename(path),
|
23
|
+
file_name: 'ftl_' + content_md5 + '_' + File.basename(path),
|
21
24
|
file_size: File.size(path),
|
22
25
|
content_type: content_type,
|
23
26
|
bytes: File.read(path)
|
@@ -70,6 +70,11 @@ module Spaceship
|
|
70
70
|
[res[1].to_i, res[2].to_i]
|
71
71
|
end
|
72
72
|
|
73
|
-
|
73
|
+
# @return (String) md5 checksum of given file
|
74
|
+
def md5digest(file_path)
|
75
|
+
Digest::MD5.hexdigest(File.read(file_path))
|
76
|
+
end
|
77
|
+
|
78
|
+
module_function :content_type, :grab_video_preview, :portrait?, :resolution, :video_resolution, :md5digest
|
74
79
|
end
|
75
80
|
end
|
@@ -32,29 +32,17 @@ module Spaceship
|
|
32
32
|
end
|
33
33
|
|
34
34
|
def send_login_request(user, password)
|
35
|
-
response =
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
end
|
47
|
-
|
48
|
-
case response.status
|
49
|
-
when 302
|
50
|
-
return response
|
51
|
-
when 200
|
52
|
-
raise InvalidUserCredentialsError.new, "Invalid username and password combination. Used '#{user}' as the username."
|
53
|
-
else
|
54
|
-
# Something went wrong. Was it invalid credentials or server issue
|
55
|
-
info = [response.body, response['Set-Cookie']]
|
56
|
-
raise UnexpectedResponse.new, info.join("\n")
|
57
|
-
end
|
35
|
+
response = send_shared_login_request(user, password)
|
36
|
+
return response if self.cookie.include?("myacinfo")
|
37
|
+
|
38
|
+
# When the user has 2 step enabled, we might have to call this method again
|
39
|
+
# This only occurs when the user doesn't have a team on iTunes Connect
|
40
|
+
# For 2 step verification we use the iTunes Connect back-end
|
41
|
+
# which is enough to get the DES... cookie, however we don't get a valid
|
42
|
+
# myacinfo cookie at that point. That means, after getting the DES... cookie
|
43
|
+
# we have to send the login request again. This will then get us a valid myacinfo
|
44
|
+
# cookie, additionally to the DES... cookie
|
45
|
+
return send_shared_login_request(user, password)
|
58
46
|
end
|
59
47
|
|
60
48
|
# @return (Array) A list of all available teams
|
@@ -237,7 +237,7 @@ module Spaceship
|
|
237
237
|
# })
|
238
238
|
#
|
239
239
|
# Available Values
|
240
|
-
# https://github.com/
|
240
|
+
# https://github.com/fastlane/fastlane/blob/master/deliver/Reference.md
|
241
241
|
def update_rating(hash)
|
242
242
|
raise "Must be a hash" unless hash.kind_of?(Hash)
|
243
243
|
|
@@ -457,6 +457,18 @@ module Spaceship
|
|
457
457
|
client.release!(self.application.apple_id, self.version_id)
|
458
458
|
end
|
459
459
|
|
460
|
+
#####################################################
|
461
|
+
# @!group Promo codes
|
462
|
+
#####################################################
|
463
|
+
def generate_promocodes!(quantity)
|
464
|
+
data = client.generate_app_version_promocodes!(
|
465
|
+
app_id: self.application.apple_id,
|
466
|
+
version_id: self.version_id,
|
467
|
+
quantity: quantity
|
468
|
+
)
|
469
|
+
Tunes::AppVersionGeneratedPromocodes.factory(data)
|
470
|
+
end
|
471
|
+
|
460
472
|
# These methods takes care of properly parsing values that
|
461
473
|
# are not returned in the right format, e.g. boolean as string
|
462
474
|
def release_on_approval
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Spaceship
|
2
|
+
module Tunes
|
3
|
+
# Represents the information about the generation of promocodes
|
4
|
+
class AppVersionGeneratedPromocodes < TunesBase
|
5
|
+
# @return
|
6
|
+
attr_reader :effective_date
|
7
|
+
attr_reader :expiration_date
|
8
|
+
attr_reader :username
|
9
|
+
# the AppVersionPromocodes this relates to
|
10
|
+
attr_reader :version
|
11
|
+
# Array of String
|
12
|
+
attr_reader :codes
|
13
|
+
|
14
|
+
attr_mapping({
|
15
|
+
'effectiveDate' => :effective_date,
|
16
|
+
'expirationDate' => :expiration_date,
|
17
|
+
'username' => :username
|
18
|
+
})
|
19
|
+
|
20
|
+
class << self
|
21
|
+
# Create a new object based on a hash.
|
22
|
+
# This is used to create a new object based on the server response.
|
23
|
+
def factory(attrs)
|
24
|
+
obj = self.new(attrs)
|
25
|
+
return obj
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def setup
|
30
|
+
@version = Tunes::AppVersionPromocodes.factory(raw_data['version'])
|
31
|
+
@codes = raw_data['codes']
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Spaceship
|
2
|
+
module Tunes
|
3
|
+
# Represents the information about remaining number of promo codes for an app version
|
4
|
+
class AppVersionPromocodes < TunesBase
|
5
|
+
# @return
|
6
|
+
attr_reader :app_id
|
7
|
+
attr_reader :app_name
|
8
|
+
attr_reader :version
|
9
|
+
attr_reader :platform
|
10
|
+
attr_reader :number_of_codes
|
11
|
+
attr_reader :maximum_number_of_codes
|
12
|
+
attr_reader :contract_file_name
|
13
|
+
|
14
|
+
attr_mapping({
|
15
|
+
'id' => :app_id,
|
16
|
+
'appName' => :app_name,
|
17
|
+
'version' => :version,
|
18
|
+
'platform' => :platform,
|
19
|
+
'numberOfCodes' => :number_of_codes,
|
20
|
+
'maximumNumberOfCodes' => :maximum_number_of_codes,
|
21
|
+
'contractFileName' => :contract_file_name
|
22
|
+
})
|
23
|
+
|
24
|
+
class << self
|
25
|
+
# Create a new object based on a hash.
|
26
|
+
# This is used to create a new object based on the server response.
|
27
|
+
def factory(attrs)
|
28
|
+
obj = self.new(attrs)
|
29
|
+
return obj
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -376,6 +376,23 @@ module Spaceship
|
|
376
376
|
tester.remove_from_app!(self.apple_id)
|
377
377
|
end
|
378
378
|
|
379
|
+
#####################################################
|
380
|
+
# @!group Promo codes
|
381
|
+
#####################################################
|
382
|
+
def promocodes
|
383
|
+
data = client.app_promocodes(app_id: self.apple_id)
|
384
|
+
data.map do |attrs|
|
385
|
+
Tunes::AppVersionPromocodes.factory(attrs)
|
386
|
+
end
|
387
|
+
end
|
388
|
+
|
389
|
+
def promocodes_history
|
390
|
+
data = client.app_promocodes_history(app_id: self.apple_id)
|
391
|
+
data.map do |attrs|
|
392
|
+
Tunes::AppVersionGeneratedPromocodes.factory(attrs)
|
393
|
+
end
|
394
|
+
end
|
395
|
+
|
379
396
|
# private to module
|
380
397
|
def ensure_not_a_bundle
|
381
398
|
# we only support applications
|
@@ -162,6 +162,7 @@ module Spaceship
|
|
162
162
|
# last_name: "Krause",
|
163
163
|
# review_email: "Contact email address for Apple",
|
164
164
|
# phone_number: "0128383383",
|
165
|
+
# review_notes: "Review notes"
|
165
166
|
#
|
166
167
|
# # Optional Metadata:
|
167
168
|
# privacy_policy_url: nil,
|
@@ -170,36 +171,18 @@ module Spaceship
|
|
170
171
|
# review_password: nil,
|
171
172
|
# encryption: false
|
172
173
|
# }
|
174
|
+
# Note that iTC will pull a lot of this information from previous builds or the app store information,
|
175
|
+
# all of the required values must be set either in this hash or automatically for this to work
|
173
176
|
def submit_for_beta_review!(metadata)
|
174
177
|
parameters = {
|
175
178
|
app_id: self.apple_id,
|
176
179
|
train: self.train_version,
|
177
180
|
build_number: self.build_version,
|
178
|
-
platform: self.platform
|
179
|
-
|
180
|
-
# Required Metadata:
|
181
|
-
changelog: "No changelog provided",
|
182
|
-
description: "No app description provided",
|
183
|
-
feedback_email: "contact@company.com",
|
184
|
-
marketing_url: "http://marketing.com",
|
185
|
-
first_name: "Felix",
|
186
|
-
last_name: "Krause",
|
187
|
-
review_email: "contact@company.com",
|
188
|
-
phone_number: "0123456789",
|
189
|
-
significant_change: false,
|
190
|
-
|
191
|
-
# Optional Metadata:
|
192
|
-
privacy_policy_url: nil,
|
193
|
-
review_user_name: nil,
|
194
|
-
review_password: nil,
|
195
|
-
encryption: false
|
181
|
+
platform: self.platform
|
196
182
|
}.merge(metadata)
|
197
183
|
|
198
184
|
client.submit_testflight_build_for_review!(parameters)
|
199
185
|
|
200
|
-
# Last, enable beta testing for this train (per iTC requirement). This will fail until the app has been approved for beta testing
|
201
|
-
self.build_train.update_testing_status!(true, 'external', self)
|
202
|
-
|
203
186
|
return parameters
|
204
187
|
end
|
205
188
|
|
@@ -0,0 +1,69 @@
|
|
1
|
+
module Spaceship
|
2
|
+
module Tunes
|
3
|
+
class RecoveryDevice < TunesBase
|
4
|
+
# @return (String) ID provided by Apple
|
5
|
+
# @example
|
6
|
+
# "1801231651"
|
7
|
+
attr_accessor :device_id
|
8
|
+
|
9
|
+
# @return (String) The name of the device
|
10
|
+
# @example
|
11
|
+
# "Felix Krause's iPhone 6"
|
12
|
+
attr_accessor :name
|
13
|
+
|
14
|
+
# @return (Bool) This device looks suspicious [add emoji here]
|
15
|
+
# this will probably always be true, otherwise the device
|
16
|
+
# doesn't show up
|
17
|
+
# @example
|
18
|
+
# true
|
19
|
+
attr_accessor :trusted
|
20
|
+
|
21
|
+
# @return (Bool)
|
22
|
+
# @example
|
23
|
+
# true
|
24
|
+
attr_accessor :status
|
25
|
+
|
26
|
+
# @return (String) Remote URL to an image representing this device
|
27
|
+
# This shows the attention to detail by Apple <3
|
28
|
+
# @example
|
29
|
+
# "https://appleid.cdn-apple.com/static/deviceImages-5.0/iPhone/iPhone8,1-e4e7e8-dadcdb/online-sourcelist__3x.png"
|
30
|
+
# @example
|
31
|
+
# "https://appleid.cdn-apple.com/appleauth/static/bin/cb2613252489/images/sms@3x.png"
|
32
|
+
attr_accessor :device_image
|
33
|
+
|
34
|
+
# @return (String)
|
35
|
+
# @example
|
36
|
+
# "iPad Air"
|
37
|
+
# @example
|
38
|
+
# nil # e.g. when it's a phone number
|
39
|
+
attr_accessor :model_name
|
40
|
+
|
41
|
+
# @return (String)
|
42
|
+
# @example
|
43
|
+
# "79"
|
44
|
+
attr_accessor :last_two_digits
|
45
|
+
|
46
|
+
# @return (Number)
|
47
|
+
# @example
|
48
|
+
# 1446488271926
|
49
|
+
attr_accessor :update_date
|
50
|
+
|
51
|
+
attr_mapping(
|
52
|
+
'id' => :device_id,
|
53
|
+
'name' => :name,
|
54
|
+
'trusted' => :trusted,
|
55
|
+
'status' => :status,
|
56
|
+
'imageLocation3x' => :device_image,
|
57
|
+
'modelName' => :model_name,
|
58
|
+
'lastTwoDigits' => :last_two_digits,
|
59
|
+
'updateDate' => :update_date
|
60
|
+
)
|
61
|
+
|
62
|
+
class << self
|
63
|
+
def factory(attrs)
|
64
|
+
return self.new(attrs)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -23,6 +23,10 @@ require 'spaceship/tunes/app_trailer'
|
|
23
23
|
require 'spaceship/tunes/tester'
|
24
24
|
require 'spaceship/tunes/app_details'
|
25
25
|
require 'spaceship/tunes/pricing_tier'
|
26
|
+
require 'spaceship/tunes/recovery_device'
|
27
|
+
|
28
|
+
require 'spaceship/tunes/app_version_promocodes'
|
29
|
+
require 'spaceship/tunes/app_version_generated_promocodes'
|
26
30
|
|
27
31
|
# File Uploads
|
28
32
|
require 'spaceship/du/utilities'
|
@@ -116,56 +116,9 @@ module Spaceship
|
|
116
116
|
end
|
117
117
|
end
|
118
118
|
|
119
|
-
def service_key
|
120
|
-
return @service_key if @service_key
|
121
|
-
# We need a service key from a JS file to properly auth
|
122
|
-
js = request(:get, "https://itunesconnect.apple.com/itc/static-resources/controllers/login_cntrl.js")
|
123
|
-
@service_key ||= js.body.match(/itcServiceKey = '(.*)'/)[1]
|
124
|
-
end
|
125
|
-
|
126
119
|
def send_login_request(user, password)
|
127
120
|
clear_user_cached_data
|
128
|
-
|
129
|
-
data = {
|
130
|
-
accountName: user,
|
131
|
-
password: password,
|
132
|
-
rememberMe: true
|
133
|
-
}
|
134
|
-
|
135
|
-
begin
|
136
|
-
response = request(:post) do |req|
|
137
|
-
req.url "https://idmsa.apple.com/appleauth/auth/signin?widgetKey=#{service_key}"
|
138
|
-
req.body = data.to_json
|
139
|
-
req.headers['Content-Type'] = 'application/json'
|
140
|
-
req.headers['X-Requested-With'] = 'XMLHttpRequest'
|
141
|
-
req.headers['Accept'] = 'application/json, text/javascript'
|
142
|
-
end
|
143
|
-
rescue UnauthorizedAccessError
|
144
|
-
raise InvalidUserCredentialsError.new, "Invalid username and password combination. Used '#{user}' as the username."
|
145
|
-
end
|
146
|
-
|
147
|
-
# get woinst, wois, and itctx cookie values
|
148
|
-
request(:get, "https://itunesconnect.apple.com/WebObjects/iTunesConnect.woa/wa/route?noext")
|
149
|
-
request(:get, "https://itunesconnect.apple.com/WebObjects/iTunesConnect.woa")
|
150
|
-
|
151
|
-
case response.status
|
152
|
-
when 403
|
153
|
-
raise InvalidUserCredentialsError.new, "Invalid username and password combination. Used '#{user}' as the username."
|
154
|
-
when 200
|
155
|
-
return response
|
156
|
-
else
|
157
|
-
if response["Location"] == "/auth" # redirect to 2 step auth page
|
158
|
-
raise "spaceship / fastlane doesn't support 2 step enabled accounts yet. Please temporary disable 2 step verification until spaceship was updated."
|
159
|
-
elsif (response.body || "").include?('invalid="true"')
|
160
|
-
# User Credentials are wrong
|
161
|
-
raise InvalidUserCredentialsError.new, "Invalid username and password combination. Used '#{user}' as the username."
|
162
|
-
elsif (response['Set-Cookie'] || "").include?("itctx")
|
163
|
-
raise "Looks like your Apple ID is not enabled for iTunes Connect, make sure to be able to login online"
|
164
|
-
else
|
165
|
-
info = [response.body, response['Set-Cookie']]
|
166
|
-
raise ITunesConnectError.new, info.join("\n")
|
167
|
-
end
|
168
|
-
end
|
121
|
+
send_shared_login_request(user, password)
|
169
122
|
end
|
170
123
|
|
171
124
|
# rubocop:disable Metrics/CyclomaticComplexity
|
@@ -178,7 +131,8 @@ module Spaceship
|
|
178
131
|
|
179
132
|
if data.fetch('sectionErrorKeys', []).count == 0 and
|
180
133
|
data.fetch('sectionInfoKeys', []).count == 0 and
|
181
|
-
data.fetch('sectionWarningKeys', []).count == 0
|
134
|
+
data.fetch('sectionWarningKeys', []).count == 0 and
|
135
|
+
data.fetch('validationErrors', []).count == 0
|
182
136
|
|
183
137
|
logger.debug("Request was successful")
|
184
138
|
end
|
@@ -203,7 +157,8 @@ module Spaceship
|
|
203
157
|
end
|
204
158
|
|
205
159
|
errors = handle_response_hash.call(data)
|
206
|
-
errors += data.fetch('sectionErrorKeys'
|
160
|
+
errors += data.fetch('sectionErrorKeys', [])
|
161
|
+
errors += data.fetch('validationErrors', [])
|
207
162
|
|
208
163
|
# Sometimes there is a different kind of error in the JSON response
|
209
164
|
# e.g. {"warn"=>nil, "error"=>["operation_failed"], "info"=>nil}
|
@@ -556,6 +511,9 @@ module Spaceship
|
|
556
511
|
def update_build_trains!(app_id, testing_type, data)
|
557
512
|
raise "app_id is required" unless app_id
|
558
513
|
|
514
|
+
# The request fails if this key is present in the data
|
515
|
+
data.delete("dailySubmissionCountByPlatform")
|
516
|
+
|
559
517
|
r = request(:post) do |req|
|
560
518
|
req.url "ra/apps/#{app_id}/testingTypes/#{testing_type}/trains/"
|
561
519
|
req.body = data.to_json
|
@@ -622,6 +580,7 @@ module Spaceship
|
|
622
580
|
handle_itc_response(r.body)
|
623
581
|
end
|
624
582
|
|
583
|
+
# rubocop:disable Metrics/ParameterLists
|
625
584
|
def submit_testflight_build_for_review!(app_id: nil, train: nil, build_number: nil, platform: 'ios',
|
626
585
|
# Required Metadata:
|
627
586
|
changelog: nil,
|
@@ -638,6 +597,7 @@ module Spaceship
|
|
638
597
|
privacy_policy_url: nil,
|
639
598
|
review_user_name: nil,
|
640
599
|
review_password: nil,
|
600
|
+
review_notes: nil,
|
641
601
|
encryption: false)
|
642
602
|
|
643
603
|
build_info = get_build_info_for_review(app_id: app_id, train: train, build_number: build_number, platform: platform)
|
@@ -660,6 +620,7 @@ module Spaceship
|
|
660
620
|
build_info['testInfo']['reviewEmail']['value'] = review_email if review_email
|
661
621
|
build_info['testInfo']['reviewUserName']['value'] = review_user_name if review_user_name
|
662
622
|
build_info['testInfo']['reviewPassword']['value'] = review_password if review_password
|
623
|
+
build_info['testInfo']['reviewNotes']['value'] = review_notes if review_notes
|
663
624
|
|
664
625
|
r = request(:post) do |req| # same URL, but a POST request
|
665
626
|
req.url "ra/apps/#{app_id}/platforms/#{platform}/trains/#{train}/builds/#{build_number}/submit/start"
|
@@ -677,6 +638,7 @@ module Spaceship
|
|
677
638
|
encryption_info: encryption_info,
|
678
639
|
encryption: encryption)
|
679
640
|
end
|
641
|
+
# rubocop:enable Metrics/ParameterLists
|
680
642
|
|
681
643
|
def get_build_info_for_review(app_id: nil, train: nil, build_number: nil, platform: 'ios')
|
682
644
|
r = request(:get) do |req|
|
@@ -864,6 +826,33 @@ module Spaceship
|
|
864
826
|
parse_response(r, 'data')
|
865
827
|
end
|
866
828
|
|
829
|
+
#####################################################
|
830
|
+
# @!group Promo codes
|
831
|
+
#####################################################
|
832
|
+
def app_promocodes(app_id: nil)
|
833
|
+
r = request(:get, "ra/apps/#{app_id}/promocodes/versions")
|
834
|
+
parse_response(r, 'data')['versions']
|
835
|
+
end
|
836
|
+
|
837
|
+
def generate_app_version_promocodes!(app_id: nil, version_id: nil, quantity: nil)
|
838
|
+
data = {
|
839
|
+
numberOfCodes: { value: quantity },
|
840
|
+
agreedToContract: { value: true }
|
841
|
+
}
|
842
|
+
url = "ra/apps/#{app_id}/promocodes/versions/#{version_id}"
|
843
|
+
r = request(:post) do |req|
|
844
|
+
req.url url
|
845
|
+
req.body = data.to_json
|
846
|
+
req.headers['Content-Type'] = 'application/json'
|
847
|
+
end
|
848
|
+
parse_response(r, 'data')
|
849
|
+
end
|
850
|
+
|
851
|
+
def app_promocodes_history(app_id: nil)
|
852
|
+
r = request(:get, "ra/apps/#{app_id}/promocodes/history")
|
853
|
+
parse_response(r, 'data')['requests']
|
854
|
+
end
|
855
|
+
|
867
856
|
private
|
868
857
|
|
869
858
|
def with_tunes_retry(tries = 5, &_block)
|
@@ -0,0 +1,155 @@
|
|
1
|
+
module Spaceship
|
2
|
+
class Client
|
3
|
+
def handle_two_step(response)
|
4
|
+
@x_apple_web_session_token = response["x-apple-web-session-token"]
|
5
|
+
@scnt = response["scnt"]
|
6
|
+
|
7
|
+
r = request(:get) do |req|
|
8
|
+
req.url "https://idmsa.apple.com/appleauth/auth"
|
9
|
+
req.headers["scnt"] = @scnt
|
10
|
+
req.headers["X-Apple-Web-Session-Token"] = @x_apple_web_session_token
|
11
|
+
req.headers["Accept"] = "application/json"
|
12
|
+
end
|
13
|
+
|
14
|
+
if r.body.kind_of?(Hash) && r.body["trustedDevices"].kind_of?(Array)
|
15
|
+
if r.body.fetch("securityCode", {})["tooManyCodesLock"].to_s.length > 0
|
16
|
+
raise ITunesConnectError.new, "Too many verification codes have been sent. Enter the last code you received, use one of your devices, or try again later."
|
17
|
+
end
|
18
|
+
|
19
|
+
old_client = (begin
|
20
|
+
Tunes::RecoveryDevice.client
|
21
|
+
rescue
|
22
|
+
nil # since client might be nil, which raises an exception
|
23
|
+
end)
|
24
|
+
Tunes::RecoveryDevice.client = self # temporary set it as it's required by the factory method
|
25
|
+
devices = r.body["trustedDevices"].collect do |current|
|
26
|
+
Tunes::RecoveryDevice.factory(current)
|
27
|
+
end
|
28
|
+
Tunes::RecoveryDevice.client = old_client
|
29
|
+
|
30
|
+
puts "Two Step Verification for account '#{self.user}' is enabled"
|
31
|
+
puts "Please select a device to verify your identity"
|
32
|
+
available = devices.collect do |c|
|
33
|
+
"#{c.name}\t#{c.model_name || 'SMS'}\t(#{c.device_id})"
|
34
|
+
end
|
35
|
+
result = choose(*available)
|
36
|
+
device_id = result.match(/.*\t.*\t\((.*)\)/)[1]
|
37
|
+
select_device(r, device_id)
|
38
|
+
else
|
39
|
+
raise "Invalid 2 step response #{r.body}"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# Only needed for 2 step
|
44
|
+
def load_session_from_file
|
45
|
+
if File.exist?(persistent_cookie_path)
|
46
|
+
puts "Loading session from '#{persistent_cookie_path}'" if $verbose
|
47
|
+
@cookie.load(persistent_cookie_path)
|
48
|
+
return true
|
49
|
+
end
|
50
|
+
return false
|
51
|
+
end
|
52
|
+
|
53
|
+
def load_session_from_env
|
54
|
+
yaml_text = ENV["FASTLANE_SESSION"] || ENV["SPACESHIP_SESSION"]
|
55
|
+
return if yaml_text.to_s.length == 0
|
56
|
+
puts "Loading session from environment variable" if $verbose
|
57
|
+
|
58
|
+
file = Tempfile.new('cookie.yml')
|
59
|
+
file.write(yaml_text.gsub("\\n", "\n"))
|
60
|
+
file.close
|
61
|
+
|
62
|
+
begin
|
63
|
+
@cookie.load(file.path)
|
64
|
+
rescue => ex
|
65
|
+
puts "Error loading session from environment"
|
66
|
+
puts "Make sure to pass the session in a valid format"
|
67
|
+
raise ex
|
68
|
+
ensure
|
69
|
+
file.unlink
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def select_device(r, device_id)
|
74
|
+
# Request Token
|
75
|
+
r = request(:put) do |req|
|
76
|
+
req.url "https://idmsa.apple.com/appleauth/auth/verify/device/#{device_id}/securitycode"
|
77
|
+
req.headers["Accept"] = "application/json"
|
78
|
+
req.headers["scnt"] = @scnt
|
79
|
+
req.headers["X-Apple-Web-Session-Token"] = @x_apple_web_session_token
|
80
|
+
end
|
81
|
+
|
82
|
+
# we use `Spaceship::TunesClient.new.handle_itc_response`
|
83
|
+
# since this might be from the Dev Portal, but for 2 step
|
84
|
+
Spaceship::TunesClient.new.handle_itc_response(r.body)
|
85
|
+
|
86
|
+
puts "Successfully requested notification"
|
87
|
+
code = ask("Please enter the 4 digit code: ")
|
88
|
+
puts "Requesting session..."
|
89
|
+
|
90
|
+
# Send token back to server to get a valid session
|
91
|
+
r = request(:post) do |req|
|
92
|
+
req.url "https://idmsa.apple.com/appleauth/auth/verify/device/#{device_id}/securitycode"
|
93
|
+
req.headers["Accept"] = "application/json"
|
94
|
+
req.headers["scnt"] = @scnt
|
95
|
+
req.headers["X-Apple-Web-Session-Token"] = @x_apple_web_session_token
|
96
|
+
req.body = { "code" => code.to_s }.to_json
|
97
|
+
req.headers['Content-Type'] = 'application/json'
|
98
|
+
end
|
99
|
+
|
100
|
+
begin
|
101
|
+
Spaceship::TunesClient.new.handle_itc_response(r.body) # this will fail if the code is invalid
|
102
|
+
rescue => ex
|
103
|
+
# If the code was entered wrong
|
104
|
+
# {
|
105
|
+
# "securityCode": {
|
106
|
+
# "code": "1234"
|
107
|
+
# },
|
108
|
+
# "securityCodeLocked": false,
|
109
|
+
# "recoveryKeyLocked": false,
|
110
|
+
# "recoveryKeySupported": true,
|
111
|
+
# "manageTrustedDevicesLinkName": "appleid.apple.com",
|
112
|
+
# "suppressResend": false,
|
113
|
+
# "authType": "hsa",
|
114
|
+
# "accountLocked": false,
|
115
|
+
# "validationErrors": [{
|
116
|
+
# "code": "-21669",
|
117
|
+
# "title": "Incorrect Verification Code",
|
118
|
+
# "message": "Incorrect verification code."
|
119
|
+
# }]
|
120
|
+
# }
|
121
|
+
if ex.to_s.include?("verification code") # to have a nicer output
|
122
|
+
puts "Error: Incorrect verification code"
|
123
|
+
return select_device(r, device_id)
|
124
|
+
end
|
125
|
+
|
126
|
+
raise ex
|
127
|
+
end
|
128
|
+
|
129
|
+
# If the request was successful, r.body is actually nil
|
130
|
+
# The previous request will fail if the user isn't on a team
|
131
|
+
# on iTunes Connect, but it still works, so we're good
|
132
|
+
|
133
|
+
# Tell iTC that we are trustworthy (obviously)
|
134
|
+
# This will update our local cookies to something new
|
135
|
+
# They probably have a longer time to live than the other poor cookies
|
136
|
+
# Changed Keys
|
137
|
+
# - myacinfo
|
138
|
+
# - DES5c148586dfd451e55afb0175f62418f91
|
139
|
+
# We actually only care about the DES value
|
140
|
+
|
141
|
+
request(:get) do |req|
|
142
|
+
req.url "https://idmsa.apple.com/appleauth/auth/2sv/trust"
|
143
|
+
req.headers["scnt"] = @scnt
|
144
|
+
req.headers["X-Apple-Web-Session-Token"] = @x_apple_web_session_token
|
145
|
+
end
|
146
|
+
# This request will fail if the user isn't added to a team on iTC
|
147
|
+
# However we don't really care, this request will still return the
|
148
|
+
# correct DES... cookie
|
149
|
+
|
150
|
+
self.store_cookie
|
151
|
+
|
152
|
+
return true
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
data/lib/spaceship/version.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: spaceship
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.25.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Felix Krause
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2016-
|
12
|
+
date: 2016-04-07 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: credentials_manager
|
@@ -137,6 +137,20 @@ dependencies:
|
|
137
137
|
- - ">="
|
138
138
|
- !ruby/object:Gem::Version
|
139
139
|
version: '0'
|
140
|
+
- !ruby/object:Gem::Dependency
|
141
|
+
name: pry-byebug
|
142
|
+
requirement: !ruby/object:Gem::Requirement
|
143
|
+
requirements:
|
144
|
+
- - ">="
|
145
|
+
- !ruby/object:Gem::Version
|
146
|
+
version: '0'
|
147
|
+
type: :development
|
148
|
+
prerelease: false
|
149
|
+
version_requirements: !ruby/object:Gem::Requirement
|
150
|
+
requirements:
|
151
|
+
- - ">="
|
152
|
+
- !ruby/object:Gem::Version
|
153
|
+
version: '0'
|
140
154
|
- !ruby/object:Gem::Dependency
|
141
155
|
name: bundler
|
142
156
|
requirement: !ruby/object:Gem::Requirement
|
@@ -193,6 +207,34 @@ dependencies:
|
|
193
207
|
- - ">="
|
194
208
|
- !ruby/object:Gem::Version
|
195
209
|
version: '0'
|
210
|
+
- !ruby/object:Gem::Dependency
|
211
|
+
name: diff_matcher
|
212
|
+
requirement: !ruby/object:Gem::Requirement
|
213
|
+
requirements:
|
214
|
+
- - ">="
|
215
|
+
- !ruby/object:Gem::Version
|
216
|
+
version: '0'
|
217
|
+
type: :development
|
218
|
+
prerelease: false
|
219
|
+
version_requirements: !ruby/object:Gem::Requirement
|
220
|
+
requirements:
|
221
|
+
- - ">="
|
222
|
+
- !ruby/object:Gem::Version
|
223
|
+
version: '0'
|
224
|
+
- !ruby/object:Gem::Dependency
|
225
|
+
name: multi_json
|
226
|
+
requirement: !ruby/object:Gem::Requirement
|
227
|
+
requirements:
|
228
|
+
- - ">="
|
229
|
+
- !ruby/object:Gem::Version
|
230
|
+
version: '0'
|
231
|
+
type: :development
|
232
|
+
prerelease: false
|
233
|
+
version_requirements: !ruby/object:Gem::Requirement
|
234
|
+
requirements:
|
235
|
+
- - ">="
|
236
|
+
- !ruby/object:Gem::Version
|
237
|
+
version: '0'
|
196
238
|
- !ruby/object:Gem::Dependency
|
197
239
|
name: rspec
|
198
240
|
requirement: !ruby/object:Gem::Requirement
|
@@ -306,7 +348,9 @@ files:
|
|
306
348
|
- lib/spaceship/tunes/app_trailer.rb
|
307
349
|
- lib/spaceship/tunes/app_version.rb
|
308
350
|
- lib/spaceship/tunes/app_version_common.rb
|
351
|
+
- lib/spaceship/tunes/app_version_generated_promocodes.rb
|
309
352
|
- lib/spaceship/tunes/app_version_history.rb
|
353
|
+
- lib/spaceship/tunes/app_version_promocodes.rb
|
310
354
|
- lib/spaceship/tunes/app_version_ref.rb
|
311
355
|
- lib/spaceship/tunes/app_version_states_history.rb
|
312
356
|
- lib/spaceship/tunes/application.rb
|
@@ -318,6 +362,7 @@ files:
|
|
318
362
|
- lib/spaceship/tunes/language_item.rb
|
319
363
|
- lib/spaceship/tunes/pricing_tier.rb
|
320
364
|
- lib/spaceship/tunes/processing_build.rb
|
365
|
+
- lib/spaceship/tunes/recovery_device.rb
|
321
366
|
- lib/spaceship/tunes/spaceship.rb
|
322
367
|
- lib/spaceship/tunes/tester.rb
|
323
368
|
- lib/spaceship/tunes/transit_app_file.rb
|
@@ -325,6 +370,7 @@ files:
|
|
325
370
|
- lib/spaceship/tunes/tunes_base.rb
|
326
371
|
- lib/spaceship/tunes/tunes_client.rb
|
327
372
|
- lib/spaceship/tunes/user_detail.rb
|
373
|
+
- lib/spaceship/two_step_client.rb
|
328
374
|
- lib/spaceship/ui.rb
|
329
375
|
- lib/spaceship/version.rb
|
330
376
|
homepage: https://fastlane.tools
|
@@ -347,7 +393,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
347
393
|
version: '0'
|
348
394
|
requirements: []
|
349
395
|
rubyforge_project:
|
350
|
-
rubygems_version: 2.
|
396
|
+
rubygems_version: 2.4.0
|
351
397
|
signing_key:
|
352
398
|
specification_version: 4
|
353
399
|
summary: Because you would rather spend your time building stuff than fighting provisioning
|