deliver 0.0.1 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +37 -10
- data/.rspec +1 -0
- data/.travis.yml +8 -0
- data/.yardopts +1 -0
- data/Gemfile.lock +106 -0
- data/LICENSE +21 -0
- data/README.md +268 -17
- data/Rakefile +3 -0
- data/assets/PDFExample.png +0 -0
- data/assets/SubmitForReviewInformation.png +0 -0
- data/assets/deliver.png +0 -0
- data/assets/deliver.pxm +0 -0
- data/assets/deliverFullSize.png +0 -0
- data/bin/deliver +32 -0
- data/deliver.gemspec +15 -9
- data/lib/assets/DeliverfileDefault +47 -0
- data/lib/assets/DeliverfileExample +62 -0
- data/lib/assets/ScreenshotsHelp +4 -0
- data/lib/deliver.rb +20 -1
- data/lib/deliver/app.rb +146 -0
- data/lib/deliver/app_metadata.rb +487 -0
- data/lib/deliver/app_screenshot.rb +119 -0
- data/lib/deliver/commands.rb +4 -0
- data/lib/deliver/commands/init.rb +12 -0
- data/lib/deliver/commands/run.rb +20 -0
- data/lib/deliver/deliver_process.rb +241 -0
- data/lib/deliver/deliverer.rb +112 -0
- data/lib/deliver/deliverfile/deliverfile.rb +35 -0
- data/lib/deliver/deliverfile/deliverfile_creator.rb +115 -0
- data/lib/deliver/deliverfile/dsl.rb +124 -0
- data/lib/deliver/dependency_checker.rb +32 -0
- data/lib/deliver/helper.rb +55 -0
- data/lib/deliver/ipa_uploader.rb +160 -0
- data/lib/deliver/itunes_connect.rb +514 -0
- data/lib/deliver/itunes_search_api.rb +48 -0
- data/lib/deliver/itunes_transporter.rb +154 -0
- data/lib/deliver/languages.rb +6 -0
- data/lib/deliver/metadata_item.rb +94 -0
- data/lib/deliver/password_manager.rb +86 -0
- data/lib/deliver/pdf_generator.rb +131 -0
- data/lib/deliver/version.rb +1 -1
- data/spec/app_metadata_spec.rb +350 -0
- data/spec/app_screenshot_spec.rb +88 -0
- data/spec/app_spec.rb +85 -0
- data/spec/deliverer_spec.rb +48 -0
- data/spec/deliverfile_creator_spec.rb +73 -0
- data/spec/example_deliver_files_spec.rb +227 -0
- data/spec/fixtures/Deliverfiles/DeliverfileCallbacks +14 -0
- data/spec/fixtures/Deliverfiles/DeliverfileCallbacksFailingTests +14 -0
- data/spec/fixtures/Deliverfiles/DeliverfileCallbacksNoErrorBlock +6 -0
- data/spec/fixtures/Deliverfiles/DeliverfileDefaultLanguageNotOnTop +8 -0
- data/spec/fixtures/Deliverfiles/DeliverfileDuplicateIpa +2 -0
- data/spec/fixtures/Deliverfiles/DeliverfileLocales +16 -0
- data/spec/fixtures/Deliverfiles/DeliverfileMetadataJson +6 -0
- data/spec/fixtures/Deliverfiles/DeliverfileMissingAppVersion +1 -0
- data/spec/fixtures/Deliverfiles/DeliverfileMissingBlockForTests +7 -0
- data/spec/fixtures/Deliverfiles/DeliverfileMissingIdentifier +1 -0
- data/spec/fixtures/Deliverfiles/DeliverfileMissingLanguage +1 -0
- data/spec/fixtures/Deliverfiles/DeliverfileMissingValue +3 -0
- data/spec/fixtures/Deliverfiles/DeliverfileMixed +37 -0
- data/spec/fixtures/Deliverfiles/DeliverfileNoVersion +3 -0
- data/spec/fixtures/Deliverfiles/DeliverfileScreenshots +6 -0
- data/spec/fixtures/Deliverfiles/DeliverfileScreenshotsFallbackDefaultLanguage +7 -0
- data/spec/fixtures/Deliverfiles/DeliverfileSimple +8 -0
- data/spec/fixtures/Deliverfiles/DeliverfileVersionMismatchPackage +8 -0
- data/spec/fixtures/Deliverfiles/DeliverfileWrongIdentifier +5 -0
- data/spec/fixtures/Deliverfiles/DeliverfileWrongVersion +5 -0
- data/spec/fixtures/Deliverfiles/metadata.json +24 -0
- data/spec/fixtures/example1.itmsp/metadata.xml +121 -0
- data/spec/fixtures/example2.itmsp/metadata.xml +54 -0
- data/spec/fixtures/ipas/Example1.ipa +0 -0
- data/spec/fixtures/metadata/ipa_result.xml +12 -0
- data/spec/fixtures/metadata/ipa_result2.xml +12 -0
- data/spec/fixtures/packages/464686641.itmsp/metadata.xml +104 -0
- data/spec/fixtures/packages/794902327.itmsp/metadata.xml +107 -0
- data/spec/fixtures/packages/878567776.itmsp/metadata.xml +104 -0
- data/spec/fixtures/screenshots/de-DE/iPhone4.png +0 -0
- data/spec/fixtures/screenshots/de-DE/iPhone6.png +0 -0
- data/spec/fixtures/screenshots/de-DE/iPhone6Plus1.png +0 -0
- data/spec/fixtures/screenshots/de-DE/iPhone6Plus2.png +0 -0
- data/spec/fixtures/screenshots/de-DE/screenshot1.png +0 -0
- data/spec/fixtures/screenshots/de-DE/screenshot2.png +0 -0
- data/spec/fixtures/screenshots/de-DE/screenshot3.png +0 -0
- data/spec/fixtures/screenshots/de-DE/screenshot5.png +0 -0
- data/spec/fixtures/screenshots/en-US/english.png +0 -0
- data/spec/fixtures/screenshots/iPhone4.png +0 -0
- data/spec/fixtures/screenshots/invalidImage.png +0 -0
- data/spec/fixtures/screenshots/screenshot1.png +0 -0
- data/spec/fixtures/screenshots/tooMany/de-DE/iPhone4 2.png +0 -0
- data/spec/fixtures/screenshots/tooMany/de-DE/iPhone4 copy 2.png +0 -0
- data/spec/fixtures/screenshots/tooMany/de-DE/iPhone4 copy 3.png +0 -0
- data/spec/fixtures/screenshots/tooMany/de-DE/iPhone4 copy 4.png +0 -0
- data/spec/fixtures/screenshots/tooMany/de-DE/iPhone4 copy 5.png +0 -0
- data/spec/fixtures/screenshots/tooMany/de-DE/iPhone4 copy 6.png +0 -0
- data/spec/fixtures/screenshots/tooMany/de-DE/iPhone4 copy.png +0 -0
- data/spec/fixtures/screenshots/tooMany/de-DE/iPhone4.png +0 -0
- data/spec/helper_spec.rb +16 -0
- data/spec/ipa_uploader_spec.rb +61 -0
- data/spec/itunes_connect_spec.rb +12 -0
- data/spec/itunes_search_api_spec.rb +24 -0
- data/spec/itunes_transporter_spec.rb +52 -0
- data/spec/languages_spec.rb +7 -0
- data/spec/metadata_item_spec.rb +36 -0
- data/spec/mocking/transporter_mocking.rb +40 -0
- data/spec/mocking/webmocking.rb +31 -0
- data/spec/password_manager_spec.rb +27 -0
- data/spec/responses/itunesLookup-.json +4 -0
- data/spec/responses/itunesLookup-0.json +4 -0
- data/spec/responses/itunesLookup-284882215.json +106 -0
- data/spec/responses/itunesLookup-at.felixkrause.iTanky.json +8 -0
- data/spec/responses/itunesLookup-com.facebook.Facebook.json +106 -0
- data/spec/responses/itunesLookup-invalid.json +4 -0
- data/spec/responses/itunesLookup-net.sunapps.invalid.json +4 -0
- data/spec/responses/transporter/download_invalid_apple_id.txt +35 -0
- data/spec/responses/transporter/download_valid_apple_id.txt +32 -0
- data/spec/responses/transporter/upload_invalid.txt +174 -0
- data/spec/responses/transporter/upload_valid.txt +290 -0
- data/spec/spec_helper.rb +23 -0
- data/tasks/rspec.rake +3 -0
- metadata +242 -8
- 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
|