deliver 0.5.0 → 0.6.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.
@@ -1,10 +0,0 @@
1
-
2
-
3
- command :init do |c|
4
- c.syntax = 'deliver init'
5
- c.description = "Creates a new Deliverfile in the current directory"
6
-
7
- c.action do |args, options|
8
- Deliver::DeliverfileCreator.create(enclosed_directory)
9
- end
10
- end
@@ -1,24 +0,0 @@
1
-
2
-
3
- command :run do |c|
4
- c.syntax = 'deliver'
5
- c.description = 'Run a deploy process using the Deliverfile in the current folder'
6
- c.option '--force', 'Runs a deployment without verifying any information (PDF file). This can be used for build servers.'
7
- c.option '--beta', 'Runs a deployment to beta build on iTunes Connect'
8
- c.option '--skip-deploy', 'Skips deployment on iTunes Connect'
9
- c.action do |args, options|
10
- path = (Deliver::Helper.fastlane_enabled?? './fastlane' : '.')
11
- Dir.chdir(path) do # switch the context
12
- if File.exists?(deliver_path)
13
- # Everything looks alright, use the given Deliverfile
14
- options.default :beta => false, :skip_deploy => false
15
- Deliver::Deliverer.new(deliver_path, force: options.force, is_beta_ipa: options.beta, skip_deploy: options.skip_deploy)
16
- else
17
- Deliver::Helper.log.warn("No Deliverfile found at path '#{deliver_path}'.")
18
- if agree("Do you want to create a new Deliverfile at the current directory? (y/n)", true)
19
- Deliver::DeliverfileCreator.create(enclosed_directory)
20
- end
21
- end
22
- end
23
- end
24
- end
@@ -1,552 +0,0 @@
1
- require 'capybara'
2
- require 'capybara/poltergeist'
3
- require 'fastimage'
4
- require 'credentials_manager/password_manager'
5
-
6
-
7
- module Deliver
8
- # Everything that can't be achived using the {Deliver::ItunesTransporter}
9
- # will be scripted using the iTunesConnect frontend.
10
- #
11
- # Every method you call here, might take a time
12
- class ItunesConnect
13
- # This error occurs only if there is something wrong with the given login data
14
- class ItunesConnectLoginError < StandardError
15
- end
16
-
17
- # This error can occur for many reaons. It is
18
- # usually raised when a UI element could not be found
19
- class ItunesConnectGeneralError < StandardError
20
- end
21
-
22
- include Capybara::DSL
23
-
24
- ITUNESCONNECT_URL = "https://itunesconnect.apple.com/"
25
- APP_DETAILS_URL = "https://itunesconnect.apple.com/WebObjects/iTunesConnect.woa/ra/ng/app/[[app_id]]"
26
-
27
- BUTTON_STRING_NEW_VERSION = "New Version"
28
- BUTTON_STRING_SUBMIT_FOR_REVIEW = "Submit for Review"
29
- BUTTON_ADD_NEW_BUILD = 'Click + to add a build before you submit your app.'
30
-
31
- WAITING_FOR_REVIEW = "Waiting For Review"
32
- PROCESSING_TEXT = "Processing"
33
-
34
- def initialize
35
- super
36
-
37
- DependencyChecker.check_dependencies
38
-
39
- Capybara.run_server = false
40
- Capybara.default_driver = :poltergeist
41
- Capybara.javascript_driver = :poltergeist
42
- Capybara.current_driver = :poltergeist
43
- Capybara.app_host = ITUNESCONNECT_URL
44
-
45
- # Since Apple has some SSL errors, we have to configure the client properly:
46
- # https://github.com/ariya/phantomjs/issues/11239
47
- Capybara.register_driver :poltergeist do |a|
48
- conf = ['--debug=no', '--ignore-ssl-errors=yes', '--ssl-protocol=TLSv1']
49
- Capybara::Poltergeist::Driver.new(a, {
50
- phantomjs_options: conf,
51
- phantomjs_logger: File.open("/tmp/poltergeist_log.txt", "a"),
52
- js_errors: false
53
- })
54
- end
55
-
56
- page.driver.headers = { "Accept-Language" => "en" }
57
-
58
- self.login
59
- end
60
-
61
- # Loggs in a user with the given login data on the iTC Frontend.
62
- # You don't need to pass a username and password. It will
63
- # Automatically be fetched using the {CredentialsManager::PasswordManager}.
64
- # This method will also automatically be called when triggering other
65
- # actions like {#open_app_page}
66
- # @param user (String) (optional) The username/email address
67
- # @param password (String) (optional) The password
68
- # @return (bool) true if everything worked fine
69
- # @raise [ItunesConnectGeneralError] General error while executing
70
- # this action
71
- # @raise [ItunesConnectLoginError] Login data is wrong
72
- def login(user = nil, password = nil)
73
- begin
74
- Helper.log.info "Logging into iTunesConnect"
75
-
76
- user ||= CredentialsManager::PasswordManager.shared_manager.username
77
- password ||= CredentialsManager::PasswordManager.shared_manager.password
78
-
79
- result = visit ITUNESCONNECT_URL
80
- raise "Could not open iTunesConnect" unless result['status'] == 'success'
81
-
82
- (wait_for_elements('#accountpassword') rescue nil) # when the user is already logged in, this will raise an exception
83
-
84
- if page.has_content?"My Apps"
85
- # Already logged in
86
- return true
87
- end
88
-
89
- fill_in "accountname", with: user
90
- fill_in "accountpassword", with: password
91
-
92
- begin
93
- (wait_for_elements(".enabled").first.click rescue nil) # Login Button
94
- wait_for_elements('.homepageWrapper.ng-scope')
95
-
96
- if page.has_content?"My Apps"
97
- # Everything looks good
98
- else
99
- raise ItunesConnectLoginError.new("Looks like your login data was correct, but you do not have access to the apps.")
100
- end
101
- rescue => ex
102
- Helper.log.debug(ex)
103
- raise ItunesConnectLoginError.new("Error logging in user #{user} with the given password. Make sure you entered them correctly.")
104
- end
105
-
106
- Helper.log.info "Successfully logged into iTunesConnect"
107
-
108
- true
109
- rescue => ex
110
- error_occured(ex)
111
- end
112
- end
113
-
114
- # Opens the app details page of the given app.
115
- # @param app (Deliver::App) the app that should be opened
116
- # @return (bool) true if everything worked fine
117
- # @raise [ItunesConnectGeneralError] General error while executing
118
- # this action
119
- # @raise [ItunesConnectLoginError] Login data is wrong
120
- def open_app_page(app)
121
- begin
122
- verify_app(app)
123
-
124
- Helper.log.info "Opening detail page for app #{app}"
125
-
126
- visit APP_DETAILS_URL.gsub("[[app_id]]", app.apple_id.to_s)
127
-
128
- wait_for_elements('.page-subnav')
129
- sleep 3
130
-
131
- if current_url.include?"wa/defaultError" # app could not be found
132
- raise "Could not open app details for app '#{app}'. Make sure you're using the correct Apple ID and the correct Apple developer account (#{CredentialsManager::PasswordManager.shared_manager.username}).".red
133
- end
134
-
135
- true
136
- rescue => ex
137
- error_occured(ex)
138
- end
139
- end
140
-
141
- # This method will fetch the current status ({Deliver::App::AppStatus})
142
- # of your app and return it. This method uses a headless browser
143
- # under the hood, so it might take some time until you get the result
144
- # @param app (Deliver::App) the app you want this information from
145
- # @raise [ItunesConnectGeneralError] General error while executing
146
- # this action
147
- # @raise [ItunesConnectLoginError] Login data is wrong
148
- def get_app_status(app)
149
- begin
150
- verify_app(app)
151
-
152
- open_app_page(app)
153
-
154
- if page.has_content?WAITING_FOR_REVIEW
155
- # That's either Upload Received or Waiting for Review
156
- if page.has_content?"To submit a new build, you must remove this version from review"
157
- return App::AppStatus::WAITING_FOR_REVIEW
158
- else
159
- return App::AppStatus::UPLOAD_RECEIVED
160
- end
161
- elsif page.has_content?BUTTON_STRING_NEW_VERSION
162
- return App::AppStatus::READY_FOR_SALE
163
- elsif page.has_content?BUTTON_STRING_SUBMIT_FOR_REVIEW
164
- return App::AppStatus::PREPARE_FOR_SUBMISSION
165
- else
166
- raise "App status not yet implemented"
167
- end
168
- rescue Exception => ex
169
- error_occured(ex)
170
- end
171
- end
172
-
173
- # This method will fetch the version number of the currently live version
174
- # of your app and return it. This method uses a headless browser
175
- # under the hood, so it might take some time until you get the result
176
- # @param app (Deliver::App) the app you want this information from
177
- # @raise [ItunesConnectGeneralError] General error while executing
178
- # this action
179
- # @raise [ItunesConnectLoginError] Login data is wrong
180
- def get_live_version(app)
181
- begin
182
- verify_app(app)
183
-
184
- open_app_page(app)
185
-
186
- begin
187
- return first(".status.ready").text.split(" ").first
188
- rescue
189
- Helper.log.debug "Could not fetch version number of the live version for app #{app}."
190
- return nil
191
- end
192
- rescue => ex
193
- error_occured(ex)
194
- end
195
- end
196
-
197
-
198
-
199
- #####################################################
200
- # @!group Constructive/Destructive Methods
201
- #####################################################
202
-
203
- # This method creates a new version of your app using the
204
- # iTunesConnect frontend. This will happen directly after calling
205
- # this method.
206
- # @param app (Deliver::App) the app you want to modify
207
- # @param version_number (String) the version number as string for
208
- # the new version that should be created
209
- def create_new_version!(app, version_number)
210
- begin
211
- current_version = get_live_version(app)
212
-
213
- verify_app(app)
214
- open_app_page(app)
215
-
216
- if page.has_content?BUTTON_STRING_NEW_VERSION
217
-
218
- if current_version == version_number
219
- # This means, this version is already live on the App Store
220
- raise "Version #{version_number} is already created, submitted and released on iTC. Please verify you're using a new version number."
221
- end
222
-
223
- click_on BUTTON_STRING_NEW_VERSION
224
-
225
- Helper.log.info "Creating a new version (#{version_number})"
226
-
227
- all(".fullWidth.nobottom.ng-isolate-scope.ng-pristine").last.set(version_number.to_s)
228
- click_on "Create"
229
-
230
- while not page.has_content?"Prepare for Submission"
231
- sleep 1
232
- Helper.log.debug("Waiting for 'Prepare for Submission'")
233
- end
234
- else
235
- Helper.log.warn "Can not create version #{version_number} on iTunesConnect. Maybe it was already created."
236
- Helper.log.info "Check out '#{current_url}' what's the latest version."
237
-
238
- begin
239
- created_version = first(".status.waiting").text.split(" ").first
240
- if created_version != version_number
241
- raise "Some other version ('#{created_version}') was created instead of the one you defined ('#{version_number}')"
242
- end
243
- rescue => ex
244
- # Can not fetch the version number of the new version (this happens, when it's e.g. 'Developer Rejected')
245
- unless page.has_content?version_number
246
- raise "Some other version was created instead of the one you defined ('#{version_number}')."
247
- end
248
- end
249
- end
250
-
251
- true
252
- rescue => ex
253
- error_occured(ex)
254
- end
255
- end
256
-
257
- # def update_app_icon!(app, path)
258
- # raise "App icon not found at path '#{path}'" unless File.exists?(path)
259
- # size = FastImage.size(path)
260
- # raise "App icon must have the resolution of 1024x1024px" unless (size[0] == 1024 and size[1] == 1024)
261
-
262
- # begin
263
- # verify_app(app)
264
- # open_app_page(app)
265
-
266
- # Capybara.ignore_hidden_elements = false
267
-
268
- # icon_area = first(:xpath, "//div[@url='tempPageContent.appIconDisplayUrl']")
269
- # delete_button = icon_area.first(".deleteButton")
270
- # input = icon_area.first(:xpath, ".//input[@type='file']")
271
-
272
-
273
- # Capybara.ignore_hidden_elements = true
274
- # first(:button, "Save").click
275
-
276
- # rescue => ex
277
- # error_occured(ex)
278
- # end
279
- # end
280
-
281
-
282
- # This will put the latest uploaded build as a new beta build
283
- def put_build_into_beta_testing!(app, version_number)
284
- begin
285
- verify_app(app)
286
- open_app_page(app)
287
-
288
- Helper.log.info("Choosing the latest build on iTunesConnect for beta distribution")
289
-
290
- click_on "Prerelease"
291
-
292
- wait_for_preprocessing
293
-
294
- if all(".switcher.ng-binding").count == 0
295
- raise "Could not find beta build on '#{current_url}'. Make sure it is available there"
296
- end
297
-
298
- if first(".switcher.ng-binding")['class'].include?"checked"
299
- Helper.log.warn("Beta Build seems to be already active. Take a look at '#{current_url}'")
300
- return true
301
- end
302
-
303
- first(".switcher.ng-binding").click
304
- if page.has_content?"Are you sure you want to start testing"
305
- click_on "Start"
306
- end
307
-
308
-
309
- return true
310
- rescue => ex
311
- error_occured(ex)
312
- end
313
- end
314
-
315
- # This will choose the latest uploaded build on iTunesConnect as the production one
316
- # After this method, you still have to call submit_for_review to actually submit the
317
- # whole update
318
- # @param app (Deliver::App) the app you want to choose the build for
319
- # @param version_number (String) the version number as string for
320
- def put_build_into_production!(app, version_number)
321
- begin
322
- verify_app(app)
323
- open_app_page(app)
324
-
325
- Helper.log.info("Choosing the latest build on iTunesConnect for release")
326
-
327
- click_on "Prerelease"
328
-
329
- wait_for_preprocessing
330
-
331
- ################# Apple is finished processing the ipa file #################
332
-
333
- Helper.log.info("Apple finally finished processing the ipa file")
334
- open_app_page(app)
335
-
336
- begin
337
- first('a', :text => BUTTON_ADD_NEW_BUILD).click
338
- wait_for_elements(".buildModalList")
339
- sleep 5
340
- rescue
341
- if page.has_content?"Upload Date"
342
- # That's fine, the ipa was already selected
343
- return true
344
- else
345
- raise "Could not find Build Button. It looks like the ipa file was not properly uploaded."
346
- end
347
- end
348
-
349
- if page.all('td', :text => version_number).count > 1
350
- Helper.log.fatal "There were multiple submitted builds found. Don't know which one to choose. Just choosing the top one for now"
351
- end
352
-
353
- result = page.first('td', :text => version_number).first(:xpath,"./..").first(:css, ".small").click
354
- click_on "Done" # Save the modal dialog
355
- click_on "Save" # on the top right to save everything else
356
-
357
- error = page.has_content?BUTTON_ADD_NEW_BUILD
358
- raise "Could not put build itself onto production. Try opening '#{current_url}'" if error
359
-
360
- return true
361
- rescue => ex
362
- error_occured(ex)
363
- end
364
- end
365
-
366
- # Submits the update itself to Apple, this includes the app metadata and the ipa file
367
- # This can easily cause exceptions, which will be shown on iTC.
368
- # @param app (Deliver::App) the app you want to submit
369
- # @param perms (Hash) information about content rights, ...
370
- def submit_for_review!(app, perms = nil)
371
- begin
372
- verify_app(app)
373
- open_app_page(app)
374
-
375
- Helper.log.info("Submitting app for Review")
376
-
377
- if not page.has_content?BUTTON_STRING_SUBMIT_FOR_REVIEW
378
- if page.has_content?WAITING_FOR_REVIEW
379
- Helper.log.info("App is already Waiting For Review")
380
- return true
381
- else
382
- raise "Couldn't find button with name '#{BUTTON_STRING_SUBMIT_FOR_REVIEW}'"
383
- end
384
- end
385
-
386
- click_on BUTTON_STRING_SUBMIT_FOR_REVIEW
387
- sleep 4
388
-
389
- errors = (all(".pagemessage.error") || []).count > 0
390
- raise "Some error occured when submitting the app for review: '#{current_url}'" if errors
391
-
392
- wait_for_elements(".savingWrapper.ng-scope.ng-pristine")
393
- wait_for_elements(".radiostyle")
394
- sleep 3
395
-
396
- if page.has_content?"Content Rights"
397
- # Looks good.. just a few more steps
398
-
399
- perms ||= {
400
- export_compliance: {
401
- encryption_updated: false,
402
- cryptography_enabled: false,
403
- is_exempt: false
404
- },
405
- third_party_content: {
406
- contains_third_party_content: false,
407
- has_rights: false
408
- },
409
- advertising_identifier: {
410
- use_idfa: false,
411
- serve_advertisement: false,
412
- attribute_advertisement: false,
413
- attribute_actions: false,
414
- limit_ad_tracking: false
415
- }
416
- }
417
-
418
- basic = "//*[@itc-radio='submitForReviewAnswers"
419
- checkbox = "//*[@itc-checkbox='submitForReviewAnswers"
420
-
421
- #####################
422
- # Export Compliance #
423
- #####################
424
- if page.has_content?"Export"
425
-
426
- if not perms[:export_compliance][:encryption_updated] and perms[:export_compliance][:cryptography_enabled]
427
- raise "encryption_updated must be enabled if cryptography_enabled is enabled!"
428
- end
429
-
430
- begin
431
- encryption_updated_control = all(:xpath, "#{basic}.exportCompliance.encryptionUpdated.value' and @radio-value='#{perms[:export_compliance][:encryption_updated]}']//input")
432
- encryption_updated_control[0].trigger('click') if encryption_updated_control.count > 0
433
- first(:xpath, "#{basic}.exportCompliance.usesEncryption.value' and @radio-value='#{perms[:export_compliance][:cryptography_enabled]}']//input").trigger('click')
434
- first(:xpath, "#{basic}.exportCompliance.isExempt.value' and @radio-value='#{perms[:export_compliance][:is_exempt]}']//input").trigger('click')
435
- rescue
436
- end
437
- end
438
-
439
- ##################
440
- # Content Rights #
441
- ##################
442
- if page.has_content?"Content Rights"
443
- if not perms[:third_party_content][:contains_third_party_content] and perms[:third_party_content][:has_rights]
444
- raise "contains_third_party_content must be enabled if has_rights is enabled".red
445
- end
446
-
447
- begin
448
- first(:xpath, "#{basic}.contentRights.containsThirdPartyContent.value' and @radio-value='#{perms[:third_party_content][:contains_third_party_content]}']//input").trigger('click')
449
- first(:xpath, "#{basic}.contentRights.hasRights.value' and @radio-value='#{perms[:third_party_content][:has_rights]}']//input").trigger('click')
450
- rescue
451
- end
452
- end
453
-
454
- ##########################
455
- # Advertising Identifier #
456
- ##########################
457
- if page.has_content?"Advertising Identifier"
458
- first(:xpath, "#{basic}.adIdInfo.usesIdfa.value' and @radio-value='#{perms[:advertising_identifier][:use_idfa]}']//a").click rescue nil
459
-
460
- if perms[:advertising_identifier][:use_idfa]
461
- if perms[:advertising_identifier][:serve_advertisement]
462
- first(:xpath, "#{checkbox}.adIdInfo.servesAds.value']//a").click
463
- end
464
- if perms[:advertising_identifier][:attribute_advertisement]
465
- first(:xpath, "#{checkbox}.adIdInfo.tracksInstall.value']//a").click
466
- end
467
- if perms[:advertising_identifier][:attribute_actions]
468
- first(:xpath, "#{checkbox}.adIdInfo.tracksAction.value']//a").click
469
- end
470
- if perms[:advertising_identifier][:limit_ad_tracking]
471
- first(:xpath, "#{checkbox}.adIdInfo.limitsTracking.value']//a").click
472
- end
473
- end
474
- end
475
-
476
-
477
- Helper.log.info("Filled out the export compliance and other information on iTC".green)
478
-
479
- click_on "Submit"
480
- sleep 5
481
-
482
- if page.has_content?WAITING_FOR_REVIEW
483
- # Everything worked :)
484
- Helper.log.info("Successfully submitted App for Review".green)
485
- return true
486
- else
487
- raise "So close, it looks like there went something wrong with the actual deployment. Checkout '#{current_url}'".red
488
- end
489
- else
490
- raise "Something is missing here.".red
491
- end
492
- return false
493
- rescue => ex
494
- error_occured(ex)
495
- end
496
- end
497
-
498
-
499
- private
500
- def verify_app(app)
501
- raise ItunesConnectGeneralError.new("No valid Deliver::App given") unless app.kind_of?Deliver::App
502
- raise ItunesConnectGeneralError.new("App is missing information (apple_id not given)") unless (app.apple_id || '').to_s.length > 5
503
- end
504
-
505
- def error_occured(ex)
506
- snap
507
- raise ex # re-raise the error after saving the snapshot
508
- end
509
-
510
- def snap
511
- path = "Error#{Time.now.to_i}.png"
512
- save_screenshot(path, :full => true)
513
- system("open '#{path}'")
514
- end
515
-
516
- # Since Apple takes for ages, after the upload is properly processed, we have to wait here
517
- def wait_for_preprocessing
518
- started = Time.now
519
-
520
- # Wait, while iTunesConnect is processing the uploaded file
521
- while page.has_content?"Uploaded"
522
- # iTunesConnect is super slow... so we have to wait...
523
- Helper.log.info("Sorry, we have to wait for iTunesConnect, since it's still processing the uploaded ipa file\n" +
524
- "If this takes longer than 45 minutes, you have to re-upload the ipa file again.\n" +
525
- "You can always open the browser page yourself: '#{current_url}'\n" +
526
- "Passed time: ~#{((Time.now - started) / 60.0).to_i} minute(s)")
527
- sleep 60
528
- visit current_url
529
- sleep 10
530
- end
531
- end
532
-
533
- def wait_for_elements(name)
534
- counter = 0
535
- results = all(name)
536
- while results.count == 0
537
- # Helper.log.debug "Waiting for #{name}"
538
- sleep 0.2
539
-
540
- results = all(name)
541
-
542
- counter += 1
543
- if counter > 100
544
- Helper.log.debug page.html
545
- Helper.log.debug caller
546
- raise ItunesConnectGeneralError.new("Couldn't find element '#{name}' after waiting for quite some time")
547
- end
548
- end
549
- return results
550
- end
551
- end
552
- end