deliver 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (122) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +37 -10
  3. data/.rspec +1 -0
  4. data/.travis.yml +8 -0
  5. data/.yardopts +1 -0
  6. data/Gemfile.lock +106 -0
  7. data/LICENSE +21 -0
  8. data/README.md +268 -17
  9. data/Rakefile +3 -0
  10. data/assets/PDFExample.png +0 -0
  11. data/assets/SubmitForReviewInformation.png +0 -0
  12. data/assets/deliver.png +0 -0
  13. data/assets/deliver.pxm +0 -0
  14. data/assets/deliverFullSize.png +0 -0
  15. data/bin/deliver +32 -0
  16. data/deliver.gemspec +15 -9
  17. data/lib/assets/DeliverfileDefault +47 -0
  18. data/lib/assets/DeliverfileExample +62 -0
  19. data/lib/assets/ScreenshotsHelp +4 -0
  20. data/lib/deliver.rb +20 -1
  21. data/lib/deliver/app.rb +146 -0
  22. data/lib/deliver/app_metadata.rb +487 -0
  23. data/lib/deliver/app_screenshot.rb +119 -0
  24. data/lib/deliver/commands.rb +4 -0
  25. data/lib/deliver/commands/init.rb +12 -0
  26. data/lib/deliver/commands/run.rb +20 -0
  27. data/lib/deliver/deliver_process.rb +241 -0
  28. data/lib/deliver/deliverer.rb +112 -0
  29. data/lib/deliver/deliverfile/deliverfile.rb +35 -0
  30. data/lib/deliver/deliverfile/deliverfile_creator.rb +115 -0
  31. data/lib/deliver/deliverfile/dsl.rb +124 -0
  32. data/lib/deliver/dependency_checker.rb +32 -0
  33. data/lib/deliver/helper.rb +55 -0
  34. data/lib/deliver/ipa_uploader.rb +160 -0
  35. data/lib/deliver/itunes_connect.rb +514 -0
  36. data/lib/deliver/itunes_search_api.rb +48 -0
  37. data/lib/deliver/itunes_transporter.rb +154 -0
  38. data/lib/deliver/languages.rb +6 -0
  39. data/lib/deliver/metadata_item.rb +94 -0
  40. data/lib/deliver/password_manager.rb +86 -0
  41. data/lib/deliver/pdf_generator.rb +131 -0
  42. data/lib/deliver/version.rb +1 -1
  43. data/spec/app_metadata_spec.rb +350 -0
  44. data/spec/app_screenshot_spec.rb +88 -0
  45. data/spec/app_spec.rb +85 -0
  46. data/spec/deliverer_spec.rb +48 -0
  47. data/spec/deliverfile_creator_spec.rb +73 -0
  48. data/spec/example_deliver_files_spec.rb +227 -0
  49. data/spec/fixtures/Deliverfiles/DeliverfileCallbacks +14 -0
  50. data/spec/fixtures/Deliverfiles/DeliverfileCallbacksFailingTests +14 -0
  51. data/spec/fixtures/Deliverfiles/DeliverfileCallbacksNoErrorBlock +6 -0
  52. data/spec/fixtures/Deliverfiles/DeliverfileDefaultLanguageNotOnTop +8 -0
  53. data/spec/fixtures/Deliverfiles/DeliverfileDuplicateIpa +2 -0
  54. data/spec/fixtures/Deliverfiles/DeliverfileLocales +16 -0
  55. data/spec/fixtures/Deliverfiles/DeliverfileMetadataJson +6 -0
  56. data/spec/fixtures/Deliverfiles/DeliverfileMissingAppVersion +1 -0
  57. data/spec/fixtures/Deliverfiles/DeliverfileMissingBlockForTests +7 -0
  58. data/spec/fixtures/Deliverfiles/DeliverfileMissingIdentifier +1 -0
  59. data/spec/fixtures/Deliverfiles/DeliverfileMissingLanguage +1 -0
  60. data/spec/fixtures/Deliverfiles/DeliverfileMissingValue +3 -0
  61. data/spec/fixtures/Deliverfiles/DeliverfileMixed +37 -0
  62. data/spec/fixtures/Deliverfiles/DeliverfileNoVersion +3 -0
  63. data/spec/fixtures/Deliverfiles/DeliverfileScreenshots +6 -0
  64. data/spec/fixtures/Deliverfiles/DeliverfileScreenshotsFallbackDefaultLanguage +7 -0
  65. data/spec/fixtures/Deliverfiles/DeliverfileSimple +8 -0
  66. data/spec/fixtures/Deliverfiles/DeliverfileVersionMismatchPackage +8 -0
  67. data/spec/fixtures/Deliverfiles/DeliverfileWrongIdentifier +5 -0
  68. data/spec/fixtures/Deliverfiles/DeliverfileWrongVersion +5 -0
  69. data/spec/fixtures/Deliverfiles/metadata.json +24 -0
  70. data/spec/fixtures/example1.itmsp/metadata.xml +121 -0
  71. data/spec/fixtures/example2.itmsp/metadata.xml +54 -0
  72. data/spec/fixtures/ipas/Example1.ipa +0 -0
  73. data/spec/fixtures/metadata/ipa_result.xml +12 -0
  74. data/spec/fixtures/metadata/ipa_result2.xml +12 -0
  75. data/spec/fixtures/packages/464686641.itmsp/metadata.xml +104 -0
  76. data/spec/fixtures/packages/794902327.itmsp/metadata.xml +107 -0
  77. data/spec/fixtures/packages/878567776.itmsp/metadata.xml +104 -0
  78. data/spec/fixtures/screenshots/de-DE/iPhone4.png +0 -0
  79. data/spec/fixtures/screenshots/de-DE/iPhone6.png +0 -0
  80. data/spec/fixtures/screenshots/de-DE/iPhone6Plus1.png +0 -0
  81. data/spec/fixtures/screenshots/de-DE/iPhone6Plus2.png +0 -0
  82. data/spec/fixtures/screenshots/de-DE/screenshot1.png +0 -0
  83. data/spec/fixtures/screenshots/de-DE/screenshot2.png +0 -0
  84. data/spec/fixtures/screenshots/de-DE/screenshot3.png +0 -0
  85. data/spec/fixtures/screenshots/de-DE/screenshot5.png +0 -0
  86. data/spec/fixtures/screenshots/en-US/english.png +0 -0
  87. data/spec/fixtures/screenshots/iPhone4.png +0 -0
  88. data/spec/fixtures/screenshots/invalidImage.png +0 -0
  89. data/spec/fixtures/screenshots/screenshot1.png +0 -0
  90. data/spec/fixtures/screenshots/tooMany/de-DE/iPhone4 2.png +0 -0
  91. data/spec/fixtures/screenshots/tooMany/de-DE/iPhone4 copy 2.png +0 -0
  92. data/spec/fixtures/screenshots/tooMany/de-DE/iPhone4 copy 3.png +0 -0
  93. data/spec/fixtures/screenshots/tooMany/de-DE/iPhone4 copy 4.png +0 -0
  94. data/spec/fixtures/screenshots/tooMany/de-DE/iPhone4 copy 5.png +0 -0
  95. data/spec/fixtures/screenshots/tooMany/de-DE/iPhone4 copy 6.png +0 -0
  96. data/spec/fixtures/screenshots/tooMany/de-DE/iPhone4 copy.png +0 -0
  97. data/spec/fixtures/screenshots/tooMany/de-DE/iPhone4.png +0 -0
  98. data/spec/helper_spec.rb +16 -0
  99. data/spec/ipa_uploader_spec.rb +61 -0
  100. data/spec/itunes_connect_spec.rb +12 -0
  101. data/spec/itunes_search_api_spec.rb +24 -0
  102. data/spec/itunes_transporter_spec.rb +52 -0
  103. data/spec/languages_spec.rb +7 -0
  104. data/spec/metadata_item_spec.rb +36 -0
  105. data/spec/mocking/transporter_mocking.rb +40 -0
  106. data/spec/mocking/webmocking.rb +31 -0
  107. data/spec/password_manager_spec.rb +27 -0
  108. data/spec/responses/itunesLookup-.json +4 -0
  109. data/spec/responses/itunesLookup-0.json +4 -0
  110. data/spec/responses/itunesLookup-284882215.json +106 -0
  111. data/spec/responses/itunesLookup-at.felixkrause.iTanky.json +8 -0
  112. data/spec/responses/itunesLookup-com.facebook.Facebook.json +106 -0
  113. data/spec/responses/itunesLookup-invalid.json +4 -0
  114. data/spec/responses/itunesLookup-net.sunapps.invalid.json +4 -0
  115. data/spec/responses/transporter/download_invalid_apple_id.txt +35 -0
  116. data/spec/responses/transporter/download_valid_apple_id.txt +32 -0
  117. data/spec/responses/transporter/upload_invalid.txt +174 -0
  118. data/spec/responses/transporter/upload_valid.txt +290 -0
  119. data/spec/spec_helper.rb +23 -0
  120. data/tasks/rspec.rake +3 -0
  121. metadata +242 -8
  122. data/LICENSE.txt +0 -22
@@ -0,0 +1,35 @@
1
+ require 'deliver/deliverfile/dsl'
2
+
3
+ module Deliver
4
+ module Deliverfile
5
+ # Deliverfile represents a Deliverfile created by a user of this library
6
+ class Deliverfile
7
+
8
+ FILE_NAME = "Deliverfile"
9
+
10
+ include Deliver::Deliverfile::Deliverfile::DSL
11
+
12
+ # The path to the used Deliverfile.
13
+ attr_accessor :path
14
+
15
+ # Loads the Deliverfile from the given path
16
+ # @param deliver_data (Deliver::Deliverer) The deliverer which handles the
17
+ # results of running this deliverfile
18
+ # @param (String) path (optional) to the file itself. This must also include the
19
+ # filename itself.
20
+ def initialize(deliver_data, path = nil)
21
+ path ||= "./#{FILE_NAME}"
22
+ raise "#{FILE_NAME} not found at path '#{path}'".red unless File.exists?(path.to_s)
23
+
24
+ self.path = path
25
+ @deliver_data = deliver_data
26
+
27
+ content = File.read(path)
28
+
29
+ eval(content) # this is okay in this case
30
+
31
+ @deliver_data.finished_executing_deliver_file
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,115 @@
1
+ module Deliver
2
+ # Helps new user quickly adopt Deliver
3
+ class DeliverfileCreator
4
+
5
+ # This method will ask the user what he wants to do
6
+ # @param deliver_path (String) The path in which the Deliverfile should be created
7
+ # @param project_name (String) The default name of the project, which is used in the generated Deliverfile
8
+ def self.create(deliver_path, project_name = nil)
9
+ deliver_file_path = [deliver_path, Deliver::Deliverfile::Deliverfile::FILE_NAME].join("/")
10
+ raise "Deliverfile already exists at path '#{deliver_file_path}'. Run 'deliver' to use Deliver.".red if File.exists?(deliver_file_path)
11
+
12
+ project_name ||= Dir.pwd.split("/").last
13
+
14
+ if agree("Do you want Deliver to automatically create the Deliverfile for you based " +
15
+ "on your current app? (y/n)", true)
16
+
17
+ puts "\n\nFirst, you need to login with your iTunesConnect credentials. ".yellow +
18
+ "\nThis is necessary to fetch the latest metadata from your app and use it to create a Deliverfile for you." +
19
+ "\nIf you have previously entered your credentials already, you will not be asked again."
20
+
21
+ if Deliver::PasswordManager.new.username and Deliver::PasswordManager.new.password
22
+ identifier = ''
23
+ while identifier.length < 3
24
+ identifier = ask("\nApp Identifier of your app (e.g. at.felixkrause.app_name): ")
25
+ end
26
+
27
+ self.create_based_on_identifier(deliver_path, identifier, project_name)
28
+ else
29
+ self.create_example_deliver_file(deliver_file_path, project_name)
30
+ end
31
+ else
32
+ self.create_example_deliver_file(deliver_file_path, project_name)
33
+ end
34
+ end
35
+
36
+ # This method is used, when the user does not want to automatically create the Deliverfile
37
+ # @param path (String) The exact path (including the file name) in which the Deliverfile should be created
38
+ # @param project_name (String) The default name of the project, which is used in the generated Deliverfile
39
+ def self.create_example_deliver_file(path, project_name)
40
+ example = File.read("#{gem_path}/lib/assets/DeliverfileExample")
41
+ example.gsub!("[[APP_NAME]]", project_name)
42
+ File.write(path, example)
43
+
44
+ FileUtils.mkdir_p './screenshots/'
45
+
46
+ puts "Successfully created new Deliverfile at '#{path}'".green
47
+ end
48
+
49
+ # This will download all the app metadata and store its data into JSON files
50
+ # @param deliver_path (String) The directory in which the Deliverfile should be created
51
+ # @param identifier (String) The app identifier we want to create Deliverfile based on
52
+ # @param project_name (String) The default name of the project, which is used in the generated Deliverfile
53
+ def self.create_based_on_identifier(deliver_path, identifier, project_name)
54
+ app = Deliver::App.new(app_identifier: identifier)
55
+ app.set_metadata_directory("/tmp") # we don't want to pollute the current folder
56
+ app.metadata # this will download the latest app metadata
57
+
58
+ file_path = [deliver_path, Deliver::Deliverfile::Deliverfile::FILE_NAME].join('/')
59
+ json = generate_deliver_file(app, deliver_path, project_name)
60
+ File.write(file_path, json)
61
+
62
+ puts "Successfully created new Deliverfile at '#{file_path}'".green
63
+ end
64
+
65
+ private
66
+ def self.gem_path
67
+ Gem::Specification.find_by_name("deliver").gem_dir
68
+ end
69
+
70
+ # This method takes care of creating a new 'deliver' folder, containg the app metadata
71
+ # and screenshots folders
72
+ def self.generate_deliver_file(app, path, project_name)
73
+ metadata_path = "#{path}/deliver/"
74
+ FileUtils.mkdir_p metadata_path
75
+
76
+ json = create_json_based_on_xml(app, metadata_path)
77
+
78
+ meta_path = "#{metadata_path}metadata.json"
79
+ File.write(meta_path, JSON.pretty_generate(json))
80
+ puts "Successfully created new metadata JSON file at '#{meta_path}'".green
81
+
82
+ # Add a README to the screenshots folder
83
+ File.write("#{metadata_path}screenshots/README.txt", File.read("#{gem_path}/lib/assets/ScreenshotsHelp"))
84
+
85
+ # Generate the final Deliverfile here
86
+ deliver = File.read("#{gem_path}/lib/assets/DeliverfileDefault")
87
+ deliver.gsub!("[[APP_IDENTIFIER]]", app.app_identifier)
88
+ deliver.gsub!("[[APP_NAME]]", project_name)
89
+
90
+ return deliver
91
+ end
92
+
93
+ # Access the app metadata and use them to create a finished Deliverfile
94
+ def self.create_json_based_on_xml(app, path)
95
+ json = {}
96
+ # Access the app metadata and use them to create a finished Deliverfile
97
+ app_name = app.metadata.information.each do |locale, current|
98
+ current.each do |key, value|
99
+ if value and value.kind_of?Hash # that does not apply for screenshots, which is an array
100
+ current[key] = value[:value]
101
+ else
102
+ current.delete(key)
103
+ end
104
+ end
105
+
106
+ json[locale] = current
107
+
108
+ # Create an empty folder for the screenshots too
109
+ FileUtils.mkdir_p "#{path}screenshots/#{locale}/"
110
+ end
111
+
112
+ return json
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,124 @@
1
+ # Inspired by https://github.com/CocoaPods/Core/blob/master/lib/cocoapods-core/podfile/dsl.rb
2
+
3
+ module Deliver
4
+ module Deliverfile
5
+ class Deliverfile
6
+ module DSL
7
+ MISSING_VALUE_ERROR_MESSAGE = "You have to pass either a value or a block to the given method."
8
+ SPECIFY_LANGUAGE_FOR_VALUE = "You have to specify the language of the given value. Either set a default language using 'default_language \"en\"' on the top of the file or pass a hash containing the language codes"
9
+
10
+ MISSING_APP_IDENTIFIER_MESSAGE = "You have to pass a valid app identifier using the Deliver file. (e.g. 'app_identifier \"net.sunapps.app\"')"
11
+ MISSING_VERSION_NUMBER_MESSAGE = "You have to pass a valid version number using the Deliver file. (e.g. 'version \"1.0\"')"
12
+ INVALID_IPA_FILE_GIVEN = "The given ipa file seems to be wrong. Make sure it's a valid ipa file."
13
+
14
+ class DeliverfileDSLError < StandardError
15
+ end
16
+
17
+ # Setting all the metadata
18
+ def method_missing(method_sym, *arguments, &block)
19
+ allowed = Deliver::Deliverer.all_available_keys_to_set
20
+ not_translated = [:ipa, :beta_ipa, :app_identifier, :apple_id, :screenshots_path, :config_json_folder, :submit_further_information]
21
+
22
+ if allowed.include?(method_sym)
23
+ value = arguments.first || block.call
24
+
25
+ unless value
26
+ Helper.log.error(caller)
27
+ Helper.log.fatal("No value or block passed to method '#{method_sym}'")
28
+ raise DeliverfileDSLError.new(MISSING_VALUE_ERROR_MESSAGE.red)
29
+ end
30
+
31
+ if value.kind_of?String and not not_translated.include?method_sym
32
+ # The user should pass a hash for multi-lang values
33
+ # Maybe he at least set a default language
34
+ if @default_language
35
+ value = { @default_language => value }
36
+ else
37
+ raise DeliverfileDSLError.new(SPECIFY_LANGUAGE_FOR_VALUE.red)
38
+ end
39
+ end
40
+
41
+ @deliver_data.set_new_value(method_sym, value)
42
+ else
43
+ # Check if it's a block (e.g. run tests)
44
+ if Deliver::Deliverer.all_available_blocks_to_set.include?method_sym
45
+ if block
46
+ @deliver_data.set_new_block(method_sym, block)
47
+ else
48
+ raise DeliverfileDSLError.new("Value for #{method_sym} must be a Ruby block. Use '#{method_sym}' do ... end.".red)
49
+ end
50
+ else
51
+ # Couldn't find this particular method
52
+ Helper.log.error("Could not find method '#{method_sym}'. Available methods: #{allowed.collect { |a| a.to_s }}")
53
+ end
54
+ end
55
+ end
56
+
57
+ # This method can be used to set a default language, which is used
58
+ # when passing a string to metadata changes, instead of a hash
59
+ # containing multiple languages.
60
+ #
61
+ # This is approach only is recommend for deployments where you are only
62
+ # supporting one language.
63
+ #
64
+ # The language itself must be included in {Deliver::Languages::ALL_LANGUAGES}.
65
+ # @example
66
+ # default_language 'en-US'
67
+ # @example
68
+ # default_language 'de-DE'
69
+ def default_language(value = nil)
70
+ # Verify, default_language is on the top of the file
71
+ already_set = @deliver_data.deliver_process.deploy_information
72
+ minimum = (already_set[:skip_pdf] ? 2 : 1) # skip_pdf + blocks
73
+ if already_set.count > minimum
74
+ raise "'default_language' must be on the top of the Deliverfile.".red
75
+ end
76
+
77
+
78
+ @default_language = value
79
+ @default_language ||= yield if block_given?
80
+ Helper.log.debug("Set default language to #{@default_language}")
81
+ @deliver_data.set_new_value(:default_language, @default_language)
82
+ end
83
+
84
+ # Pass the path to the ipa file which should be uploaded
85
+ # @raise (DeliverfileDSLError) occurs when you pass an invalid path to the
86
+ # IPA file.
87
+ def ipa(value = nil)
88
+ value ||= yield if block_given?
89
+ validate_ipa(value)
90
+
91
+ @deliver_data.set_new_value(Deliverer::ValKey::IPA, value)
92
+ end
93
+
94
+ # Pass the path to the ipa file (beta version) which should be uploaded
95
+ # @raise (DeliverfileDSLError) occurs when you pass an invalid path to the
96
+ # IPA file.
97
+ def beta_ipa(value = nil)
98
+ value ||= yield if block_given?
99
+ validate_ipa(value)
100
+
101
+ @deliver_data.set_new_value(Deliverer::ValKey::BETA_IPA, value)
102
+ end
103
+
104
+ # Set the apps new version number.
105
+ #
106
+ # If you do not set this, it will automatically being fetched from the
107
+ # IPA file.
108
+ def version(value = nil)
109
+ value ||= yield if block_given?
110
+ raise DeliverfileDSLError.new(MISSING_VALUE_ERROR_MESSAGE.red) unless value
111
+ raise DeliverfileDSLError.new("The app version should be a string".red) unless value.kind_of?(String)
112
+
113
+ @deliver_data.set_new_value(Deliverer::ValKey::APP_VERSION, value)
114
+ end
115
+
116
+ private
117
+ def validate_ipa(value)
118
+ raise DeliverfileDSLError.new(INVALID_IPA_FILE_GIVEN.red) unless value
119
+ raise DeliverfileDSLError.new(INVALID_IPA_FILE_GIVEN.red) unless value.include?".ipa"
120
+ end
121
+ end
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,32 @@
1
+ module Deliver
2
+ class DependencyChecker
3
+ def self.check_dependencies
4
+ self.check_phantom_js
5
+ self.check_xcode_select
6
+ end
7
+
8
+ def self.check_phantom_js
9
+ if `which phantomjs`.length == 0
10
+ # Missing brew dependency
11
+ Helper.log.fatal '#############################################################'
12
+ Helper.log.fatal "# You have to install phantomjs to use deliver"
13
+ Helper.log.fatal "# phantomjs is used to control the iTunesConnect frontend"
14
+ Helper.log.fatal "# Install Homebrew using http://brew.sh/" if `which brew`.length == 0
15
+ Helper.log.fatal "# Run 'brew update && brew install phantomjs' and start deliver again"
16
+ Helper.log.fatal '#############################################################'
17
+ raise "Run 'brew update && brew install phantomjs' and start deliver again"
18
+ end
19
+ end
20
+
21
+ def self.check_xcode_select
22
+ unless `xcode-select -v`.include?"xcode-select version "
23
+ Helper.log.fatal '#############################################################'
24
+ Helper.log.fatal "# You have to install the Xcode commdand line tools to use deliver"
25
+ Helper.log.fatal "# Install the latest version of Xcode from the AppStore"
26
+ Helper.log.fatal "# Run xcode-select --install to install the developer tools"
27
+ Helper.log.fatal '#############################################################'
28
+ raise "Run 'xcode-select --install' and start deliver again"
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,55 @@
1
+ require 'logger'
2
+
3
+ module Deliver
4
+ module Helper
5
+
6
+ # Logging happens using this method
7
+ def self.log
8
+ if is_test?
9
+ @@log ||= Logger.new(nil) # don't show any logs when running tests
10
+ else
11
+ @@log ||= Logger.new(STDOUT)
12
+ end
13
+
14
+ @@log.formatter = proc do |severity, datetime, progname, msg|
15
+ string = "#{severity} [#{datetime.strftime('%Y-%m-%d %H:%M:%S.%2N')}]: "
16
+ second = "#{msg}\n"
17
+
18
+ if severity == "DEBUG"
19
+ string = string.magenta
20
+ elsif severity == "INFO"
21
+ string = string.white
22
+ elsif severity == "WARN"
23
+ string = string.yellow
24
+ elsif severity == "ERROR"
25
+ string = string.red
26
+ elsif severity == "FATAL"
27
+ string = string.red.bold
28
+ end
29
+
30
+
31
+ [string, second].join("")
32
+ end
33
+
34
+ @@log
35
+ end
36
+
37
+ # @return true if the currently running program is a unit test
38
+ def self.is_test?
39
+ defined?SpecHelper
40
+ end
41
+
42
+ # @return the full path to the Xcode developer tools of the currently
43
+ # running system
44
+ def self.xcode_path
45
+ return "" if self.is_test? and not OS.mac?
46
+ `xcode-select -p`.gsub("\n", '') + "/"
47
+ end
48
+
49
+ # @return the full path to the iTMSTransporter executable
50
+ def self.transporter_path
51
+ self.xcode_path + '../Applications/Application\ Loader.app/Contents/MacOS/itms/bin/iTMSTransporter'
52
+ end
53
+
54
+ end
55
+ end
@@ -0,0 +1,160 @@
1
+ require 'zip'
2
+ require 'plist'
3
+
4
+ module Deliver
5
+ class IpaUploaderError < StandardError
6
+ end
7
+
8
+ # This class takes care of preparing and uploading the given ipa file
9
+ # Metadata + IPA file can not be handled in one file
10
+ class IpaUploader < AppMetadata
11
+ attr_accessor :app
12
+
13
+ # Create a new uploader for one ipa file. This will only upload the ipa and no
14
+ # other app metadata.
15
+ # @param app (Deliver::App) The app for which the ipa should be uploaded for
16
+ # @param dir (String) The path to where we can store (copy) the ipa file. Usually /tmp/
17
+ # @param ipa_path (String) The path to the IPA file which should be uploaded
18
+ # @param is_beta_build (Bool) If it's a beta build, it will be released to the testers, otherwise into production
19
+ # @raise (IpaUploaderError) Is thrown when the ipa file was not found or is not valid
20
+ def initialize(app, dir, ipa_path, is_beta_build)
21
+ ipa_path.strip! # remove unused white spaces
22
+ raise IpaUploaderError.new("IPA on path '#{ipa_path}' not found") unless File.exists?(ipa_path)
23
+ raise IpaUploaderError.new("IPA on path '#{ipa_path}' is not a valid IPA file") unless ipa_path.include?".ipa"
24
+
25
+ super(app, dir, false)
26
+
27
+ @ipa_file = Deliver::MetadataItem.new(ipa_path)
28
+ @is_beta_build = is_beta_build
29
+ end
30
+
31
+ # Fetches the app identifier (e.g. com.facebook.Facebook) from the given ipa file.
32
+ def fetch_app_identifier
33
+ plist = fetch_info_plist_file
34
+ return plist['CFBundleIdentifier'] if plist
35
+ return nil
36
+ end
37
+
38
+ # Fetches the app version from the given ipa file.
39
+ def fetch_app_version
40
+ plist = fetch_info_plist_file
41
+ return plist['CFBundleShortVersionString'] if plist
42
+ return nil
43
+ end
44
+
45
+
46
+ #####################################################
47
+ # @!group Uploading the ipa file
48
+ #####################################################
49
+
50
+ # Actually upload the ipa file to Apple
51
+ # @param submit_information (Hash) A hash containing submit information (export, content rights)
52
+ def upload!(submit_information = nil)
53
+ Helper.log.info "Uploading ipa file to iTunesConnect"
54
+ build_document
55
+
56
+ # Write the current XML state to disk
57
+ folder_name = "#{@app.apple_id}.itmsp"
58
+ path = "#{@metadata_dir}/#{folder_name}/"
59
+ FileUtils.mkdir_p path
60
+
61
+ File.write("#{path}/#{METADATA_FILE_NAME}", @data.to_xml)
62
+
63
+ @ipa_file.store_file_inside_package(path)
64
+
65
+ is_okay = true
66
+ begin
67
+ transporter.upload(@app, @metadata_dir)
68
+ rescue Exception => ex
69
+ is_okay = ex.to_s.include?"ready exists a binary upload with build" # this just means, the ipa is already online
70
+ end
71
+
72
+ if is_okay
73
+ unless Helper.is_test?
74
+ return publish_on_itunes_connect(submit_information)
75
+ end
76
+ end
77
+
78
+ return is_okay
79
+ end
80
+
81
+
82
+
83
+ private
84
+ # This method will trigger the iTunesConnect class to choose the latest build
85
+ def publish_on_itunes_connect(submit_information = nil)
86
+ if not @is_beta_build
87
+ return publish_production_build(submit_information)
88
+ else
89
+ return publish_beta_build
90
+ end
91
+ return false
92
+ end
93
+
94
+ def publish_beta_build
95
+ # Distribute to beta testers
96
+ Helper.log.info "Distributing the latest build to Beta Testers."
97
+ if self.app.itc.put_build_into_beta_testing!(self.app, self.fetch_app_version)
98
+ Helper.log.info "Successfully distributed a new beta build of your app.".green
99
+ return true
100
+ end
101
+ return false
102
+ end
103
+
104
+ def publish_production_build(submit_information)
105
+ # Publish onto Production
106
+ Helper.log.info "Putting the latest build onto production."
107
+ if self.app.itc.put_build_into_production!(self.app, self.fetch_app_version)
108
+ if self.app.itc.submit_for_review!(self.app, submit_information)
109
+ Helper.log.info "Successfully deployed a new update of your app. You can now enjoy a good cold Club Mate.".green
110
+ return true
111
+ end
112
+ end
113
+ return false
114
+ end
115
+
116
+
117
+ def build_document
118
+ builder = Nokogiri::XML::Builder.new(encoding: 'UTF-8') do |xml|
119
+ xml.package(xmlns: "http://apple.com/itunes/importer", version: "software4.7") {
120
+ xml.software_assets(apple_id: @app.apple_id) {
121
+ xml.asset(type: 'bundle') {
122
+
123
+ }
124
+ }
125
+ }
126
+ end
127
+
128
+ @data = builder.doc
129
+ asset = @data.xpath('//x:asset', "x" => Deliver::AppMetadata::ITUNES_NAMESPACE).first
130
+ asset << @ipa_file.create_xml_node(@data)
131
+ end
132
+
133
+ def fetch_info_plist_file
134
+ Zip::File.open(@ipa_file.path) do |zipfile|
135
+ zipfile.each do |file|
136
+ if file.name.include?'.plist'
137
+ # We can not be completely sure, that's the correct plist file, so we have to try
138
+ begin
139
+ # The XML file has to be properly unpacked first
140
+ tmp_path = "/tmp/deploytmp.plist"
141
+ File.write(tmp_path, zipfile.read(file))
142
+ system("plutil -convert xml1 #{tmp_path}")
143
+ result = Plist::parse_xml(tmp_path)
144
+ File.delete(tmp_path)
145
+
146
+ if result['CFBundleIdentifier'] or result['CFBundleVersion']
147
+ return result
148
+ end
149
+ rescue
150
+ # We don't really care, look for another XML file
151
+ end
152
+ end
153
+ end
154
+ end
155
+
156
+ nil
157
+ end
158
+
159
+ end
160
+ end