apprepo 0.0.2 → 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,39 +1,35 @@
1
1
  module AppRepo
2
+ class Manifest
3
+ #
4
+ # Translated internal key names from Fastlane to AppRepo
5
+ #
2
6
 
3
- class Manifest
7
+ attr_accessor :appcode # AppRepo Internal Code
8
+ attr_accessor :filename # IPA file name
9
+ attr_accessor :bundle_identifier # app_identifier
10
+ attr_accessor :bundle_version # app_version
11
+ attr_accessor :title # app_name
12
+ attr_accessor :subtitle # app_description
13
+ attr_accessor :notify # will send push notification / slack
4
14
 
5
- #
6
- # Translated internal key names from Fastlane to AppRepo
7
- #
15
+ def initialize(appcode)
16
+ self.appcode = appcode
17
+ UI.message('Initializing "AppRepo:Manifest requies at least APPCODE :"' + self.appcode)
18
+ end
8
19
 
9
- attr_accessor :appcode # AppRepo Internal Code
10
- attr_accessor :filename # IPA file name
11
- attr_accessor :bundle_identifier # app_identifier
12
- attr_accessor :bundle_version # app_version
13
- attr_accessor :title # app_name
14
- attr_accessor :subtitle # app_description
15
- attr_accessor :notify # will send push notification / slack
16
-
17
- def initialize (appcode)
18
- self.appcode = appcode
19
- puts 'Initializing "AppRepo:Manifest requies at least APPCODE :"'+self.appcode
20
- end
21
-
22
- # Provide JSON serialized data
23
- def getJSON
20
+ # Provide JSON serialized data
21
+ def getJSON
24
22
  structure = {
25
- appcode: self.appcode,
26
- filename: self.filename,
27
- bundle_identifier: self.bundle_identifier,
28
- bundle_version: self.bundle_version,
29
- title: self.title,
30
- subtitle: self.subtitle,
31
- notify: self.notify
23
+ appcode: appcode,
24
+ filename: filename,
25
+ bundle_identifier: bundle_identifier,
26
+ bundle_version: bundle_version,
27
+ title: title,
28
+ subtitle: subtitle,
29
+ notify: notify
32
30
  }
33
-
31
+
34
32
  fputs structure
33
+ end
35
34
  end
36
-
37
- end
38
35
  end
39
-
@@ -0,0 +1,98 @@
1
+ require 'fastlane_core'
2
+
3
+ module AppRepo
4
+ class Options
5
+ def self.available_options
6
+ [
7
+ FastlaneCore::ConfigItem.new(key: :ipa,
8
+ short_option: '-i',
9
+ optional: true,
10
+ env_name: 'APPREPO_IPA_PATH',
11
+ description: 'Path to your ipa file',
12
+ default_value: Dir['*.ipa'].first,
13
+ verify_block: proc do |value|
14
+ UI.user_error!("Could not find ipa file at path '#{value}'") unless File.exist?(value)
15
+ UI.user_error!("'#{value}' doesn't seem to be an ipa file") unless value.end_with?('.ipa')
16
+ end,
17
+ conflicting_options: [:pkg],
18
+ conflict_block: proc do |value|
19
+ UI.user_error!("You can't use 'ipa' and '#{value.key}' options in one run.")
20
+ end),
21
+ FastlaneCore::ConfigItem.new(key: :app_identifier,
22
+ short_option: '-b',
23
+ optional: false,
24
+ env_name: 'APPREPO_APP_ID',
25
+ description: 'Your bundle identifier',
26
+ default_value: ""),
27
+ FastlaneCore::ConfigItem.new(key: :app_code,
28
+ short_option: '-c',
29
+ optional: true,
30
+ env_name: 'APPREPO_APPCODE',
31
+ description: 'APPCODE value for apprepo'),
32
+ FastlaneCore::ConfigItem.new(key: :repo_url,
33
+ short_option: '-r',
34
+ optional: false,
35
+ env_name: 'APPREPO_URL',
36
+ description: 'URL of your Apprepo server'),
37
+ FastlaneCore::ConfigItem.new(key: :repo_user,
38
+ short_option: '-u',
39
+ optional: false,
40
+ env_name: 'APPREPO_USER',
41
+ description: 'USER of your Apprepo server'),
42
+ FastlaneCore::ConfigItem.new(key: :repo_key,
43
+ short_option: '-k',
44
+ optional: false,
45
+ env_name: 'APPREPO_KEY',
46
+ description: 'RSA key for your Apprepo server'),
47
+ FastlaneCore::ConfigItem.new(key: :repo_description,
48
+ short_option: '-d',
49
+ optional: true,
50
+ env_name: 'APPREPO_DESCRIPTION',
51
+ description: 'Long description for your Apprepo server'),
52
+ FastlaneCore::ConfigItem.new(key: :metadata_path,
53
+ short_option: '-m',
54
+ description: 'Path to the folder containing the metadata files',
55
+ optional: true),
56
+ FastlaneCore::ConfigItem.new(key: :meta_title,
57
+ short_option: '-a',
58
+ description: 'Name of the app',
59
+ optional: true),
60
+ FastlaneCore::ConfigItem.new(key: :skip_binary_upload,
61
+ description: 'Skip uploading an ipa or pkg to iTunes Connect',
62
+ is_string: false,
63
+ default_value: false),
64
+ FastlaneCore::ConfigItem.new(key: :app_version,
65
+ short_option: '-z',
66
+ description: 'The version that should be edited or created',
67
+ optional: true),
68
+ FastlaneCore::ConfigItem.new(key: :skip_metadata,
69
+ description: "Don't upload the metadata (e.g. title, description), this will still upload screenshots",
70
+ is_string: false,
71
+ default_value: false),
72
+ FastlaneCore::ConfigItem.new(key: :notify,
73
+ description: "Notify AppRepo users on update",
74
+ is_string: false,
75
+ default_value: false),
76
+ FastlaneCore::ConfigItem.new(key: :build_number,
77
+ short_option: '-n',
78
+ description: 'If set the given build number (already uploaded to iTC) will be used instead of the current built one',
79
+ optional: true,
80
+ conflicting_options: [:ipa, :pkg],
81
+ conflict_block: proc do |value|
82
+ UI.user_error!("You can't use 'build_number' and '#{value.key}' options in one run.")
83
+ end),
84
+
85
+ # App Metadata
86
+ # Non Localised
87
+ FastlaneCore::ConfigItem.new(key: :app_icon,
88
+ description: 'Metadata: The path to the app icon',
89
+ optional: true,
90
+ short_option: '-l',
91
+ verify_block: proc do |value|
92
+ UI.user_error!("Could not find png file at path '#{value}'") unless File.exist?(value)
93
+ UI.user_error!("'#{value}' doesn't seem to be a png file") unless value.end_with?('.png')
94
+ end)
95
+ ]
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,77 @@
1
+ module AppRepo
2
+ class Runner
3
+ attr_accessor :options
4
+
5
+ def initialize(options)
6
+ UI.message('[AppRepo:Runner] Initializing...')
7
+ self.options = options
8
+ #login
9
+ AppRepo::DetectValues.new.run!(self.options)
10
+ #FastlaneCore::PrintTable.print_values(config: options, hide_keys: [:app], mask_keys: ['app_review_information.demo_password'], title: "deliver #{AppRepo::VERSION} Summary")
11
+ end
12
+
13
+ def login
14
+ #UI.message("Login to AppRepo (#{options[:username]})")
15
+ #AppRepo::Server.login(options[:username])
16
+ #AppRepo::Server.select_app
17
+ #UI.message('Login successful')
18
+ end
19
+
20
+ def run
21
+ verify_version if options[:app_version].to_s.length > 0
22
+ upload_metadata
23
+
24
+ has_binary = (options[:ipa] || options[:pkg])
25
+ if !options[:skip_binary_upload] && !options[:build_number] && has_binary
26
+ upload_binary
27
+ end
28
+
29
+ UI.success('Finished the upload to AppRepo')
30
+
31
+ notify if options[:notify]
32
+ end
33
+
34
+ # Make sure the version on AppRepo matches the one in the ipa
35
+ # If not, the new version will automatically be created
36
+ def verify_version
37
+ app_version = options[:app_version]
38
+ UI.message("TODO: Make sure the latest version on AppRepo matches '#{app_version}' from the ipa file...")
39
+
40
+ #changed = options[:app].ensure_version!(app_version)
41
+ #if changed
42
+ # UI.success("Successfully set the version to '#{app_version}'")
43
+ #else
44
+ # UI.success("'#{app_version}' is the latest version on iTunes Connect")
45
+ #end
46
+ end
47
+
48
+ # Upload all metadata, screenshots, pricing information, etc. to iTunes Connect
49
+ def upload_metadata
50
+ #
51
+ end
52
+
53
+ # Upload the binary to iTunes Connect
54
+ def upload_binary
55
+ UI.message('Uploading binary to iTunes Connect')
56
+ if options[:ipa]
57
+ #package_path = FastlaneCore::IpaUploadPackageBuilder.new.generate(
58
+ # app_id: options[:app].apple_id,
59
+ # ipa_path: options[:ipa],
60
+ # package_path: '/tmp'
61
+ puts "TODO: Build package path without IpaUploadPackageBuilder"
62
+
63
+ end
64
+
65
+ #transporter = FastlaneCore::ItunesTransporter.new(options[:username])
66
+ #result = transporter.upload(options[:app].apple_id, package_path)
67
+ #UI.user_error!('Could not upload binary to iTunes Connect. Check out the error above') unless result
68
+ end
69
+
70
+ def notify
71
+ # should be in metadata
72
+ end
73
+
74
+ private
75
+
76
+ end
77
+ end
@@ -0,0 +1,49 @@
1
+ module AppRepo
2
+ class Setup
3
+
4
+ def setup_apprepo(file_path, data, _apprepo_path, _options)
5
+ UI.message('[AppRepo:Setup] Setting up...')
6
+ File.write(file_path, data)
7
+
8
+ # TODO: implement later
9
+ # download_metadata(apprepo_path, options)
10
+
11
+ UI.success("NOT! created new Repofile at path '#{file_path}'")
12
+ end
13
+
14
+ # This method takes care of creating a new 'apprepo' folder, containg the app metadata
15
+ # and screenshots folders
16
+ def generate_apprepo_file(apprepo_path, options)
17
+ #
18
+ #v = options[:app].latest_version
19
+ #generate_metadata_files(v, File.join(apprepo_path, 'metadata'))
20
+
21
+ # Generate the final Repofile here
22
+ gem_path = Helper.gem_path('apprepo')
23
+ apprepo = File.read("#{gem_path}/../assets/RepofileDefault")
24
+ apprepo.gsub!("[[APP_IDENTIFIER]]", options[:app].bundle_id)
25
+ apprepo.gsub!("[[APPREPO_IPA_PATH]]", options[:app].file_path)
26
+ apprepo.gsub!("[[APP_VERSION]]", options[:app].version)
27
+ apprepo.gsub!("[[APP_NAME]]", options[:app].name)
28
+ UI.user_error("TODO: ADJUST Repofile'")
29
+ # deliver => apprepo??
30
+ end
31
+
32
+ def download_metadata(apprepo_path, _options)
33
+ path = File.join(apprepo_path, 'metadata')
34
+ FileUtils.mkdir_p(path)
35
+ UI.success("TODO: DOWNLOAD METADATA'")
36
+ # AppRepo::DownloadManifest.run(options, path)
37
+ end
38
+
39
+ def run(options)
40
+ UI.message('[AppRepo:Setup] Running...')
41
+ containing = (File.directory?('fastlane') ? 'fastlane' : '.')
42
+ file_path = File.join(containing, 'Repofile')
43
+ data = generate_apprepo_file(containing, options)
44
+ setup_apprepo(file_path, data, containing, options)
45
+ end
46
+ end
47
+ end
48
+
49
+ # @setup = new AppRepo::Setup
@@ -0,0 +1,22 @@
1
+ module AppRepo
2
+ class UploadAssets
3
+ def upload(options)
4
+ app = options[:app]
5
+
6
+ v = app.edit_version
7
+ UI.user_error!("Could not find a version to edit for app '#{app.name}'") unless v
8
+
9
+ if options[:app_icon]
10
+ UI.message('Uploading app icon...')
11
+ v.upload_large_icon!(options[:app_icon])
12
+ end
13
+
14
+ if options[:apple_watch_app_icon]
15
+ UI.message('Uploading apple watchapp icon...')
16
+ v.upload_watch_icon!(options[:apple_watch_app_icon])
17
+ end
18
+
19
+ v.save!
20
+ end
21
+ end
22
+ end
@@ -1,14 +1,13 @@
1
1
  module AppRepo
2
+ class UploadDescriptor
2
3
 
3
- class UploadDescriptor
4
+ attr_accessor :appcode # required
5
+ attr_accessor :ipa # can be inferred anyway (glob or metadata)
6
+ attr_accessor :metadata # optional, allows re-uploading same binary without metadata change
4
7
 
5
- attr_accessor :appcode
6
-
7
- def initialize (appcode)
8
- self.appcode = appcode
9
- puts 'Initializing "AppRepo:UploadDescriptor with appcode "'+self.appcode
8
+ def initialize(appcode)
9
+ self.appcode = appcode
10
+ UI.message('Initializing "AppRepo:UploadDescriptor with appcode "' + self.appcode + '"')
11
+ end
10
12
  end
11
13
  end
12
-
13
- end
14
-
@@ -0,0 +1,192 @@
1
+ module AppRepo
2
+ # upload description, rating, etc.
3
+ class UploadMetadata
4
+ # All the localised values attached to the version
5
+ LOCALISED_VERSION_VALUES = [:description, :keywords, :release_notes, :support_url, :marketing_url].freeze
6
+
7
+ # Everything attached to the version but not being localised
8
+ NON_LOCALISED_VERSION_VALUES = [:copyright].freeze
9
+
10
+ # Localised app details values
11
+ LOCALISED_APP_VALUES = [:name, :privacy_url].freeze
12
+
13
+ # Non localized app details values
14
+ NON_LOCALISED_APP_VALUES = [:primary_category, :secondary_category,
15
+ :primary_first_sub_category, :primary_second_sub_category,
16
+ :secondary_first_sub_category, :secondary_second_sub_category
17
+ ].freeze
18
+
19
+ # Make sure to call `load_from_filesystem` before calling upload
20
+ def upload(options)
21
+ return if options[:skip_metadata]
22
+ verify_available_languages!(options)
23
+
24
+ app = options[:app]
25
+
26
+ details = app.details
27
+ v = app.edit_version
28
+
29
+ (LOCALISED_VERSION_VALUES + LOCALISED_APP_VALUES).each do |key|
30
+ current = options[key]
31
+ next unless current
32
+
33
+ unless current.is_a?(Hash)
34
+ UI.error("Error with provided '#{key}'. Must be a hash, the key being the language.")
35
+ next
36
+ end
37
+
38
+ current.each do |language, value|
39
+ next unless value.to_s.length > 0
40
+ strip_value = value.to_s.strip
41
+ v.send(key)[language] = strip_value if LOCALISED_VERSION_VALUES.include?(key)
42
+ details.send(key)[language] = strip_value if LOCALISED_APP_VALUES.include?(key)
43
+ end
44
+ end
45
+
46
+ (NON_LOCALISED_VERSION_VALUES + NON_LOCALISED_APP_VALUES).each do |key|
47
+ current = options[key].to_s.strip
48
+ next unless current.to_s.length > 0
49
+ v.send("#{key}=", current) if NON_LOCALISED_VERSION_VALUES.include?(key)
50
+ details.send("#{key}=", current) if NON_LOCALISED_APP_VALUES.include?(key)
51
+ end
52
+
53
+ v.release_on_approval = options[:automatic_release]
54
+
55
+ set_review_information(v, options)
56
+ set_app_rating(v, options)
57
+
58
+ UI.message('Uploading metadata to iTunes Connect')
59
+ v.save!
60
+ details.save!
61
+ UI.success('Successfully uploaded initial set of metadata to iTunes Connect')
62
+ end
63
+
64
+ # If the user is using the 'default' language, then assign values where they are needed
65
+ def assign_defaults(options)
66
+ # Build a complete list of the required languages
67
+ enabled_languages = []
68
+
69
+ # Get all languages used in existing settings
70
+ (LOCALISED_VERSION_VALUES + LOCALISED_APP_VALUES).each do |key|
71
+ current = options[key]
72
+ next unless current && current.is_a?(Hash)
73
+ current.each do |language, _value|
74
+ enabled_languages << language unless enabled_languages.include?(language)
75
+ end
76
+ end
77
+
78
+ # Check folder list (an empty folder signifies a language is required)
79
+ Dir.glob(File.join(options[:metadata_path], '*')).each do |lng_folder|
80
+ next unless File.directory?(lng_folder) # We don't want to read txt as they are non localised
81
+
82
+ language = File.basename(lng_folder)
83
+ enabled_languages << language unless enabled_languages.include?(language)
84
+ end
85
+
86
+ return unless enabled_languages.include?('default')
87
+ UI.message('Detected languages: ' + enabled_languages.to_s)
88
+
89
+ (LOCALISED_VERSION_VALUES + LOCALISED_APP_VALUES).each do |key|
90
+ current = options[key]
91
+ next unless current && current.is_a?(Hash)
92
+
93
+ default = current['default']
94
+ next if default.nil?
95
+
96
+ enabled_languages.each do |language|
97
+ value = current[language]
98
+ next unless value.nil?
99
+
100
+ current[language] = default
101
+ end
102
+ current.delete('default')
103
+ end
104
+ end
105
+
106
+ # Makes sure all languages we need are actually created
107
+ def verify_available_languages!(options)
108
+ return if options[:skip_metadata]
109
+
110
+ # Collect all languages we need
111
+ # We only care about languages from user provided values
112
+ # as the other languages are on iTC already anyway
113
+ v = options[:app].edit_version
114
+ UI.user_error!("Could not find a version to edit for app '#{options[:app].name}', the app metadata is read-only currently") unless v
115
+
116
+ enabled_languages = []
117
+ LOCALISED_VERSION_VALUES.each do |key|
118
+ current = options[key]
119
+ next unless current && current.is_a?(Hash)
120
+ current.each do |language, _value|
121
+ enabled_languages << language unless enabled_languages.include?(language)
122
+ end
123
+ end
124
+
125
+ if enabled_languages.count > 0
126
+ v.create_languages(enabled_languages)
127
+ lng_text = 'language'
128
+ lng_text += 's' if enabled_languages.count != 1
129
+ UI.message("Activating #{lng_text} #{enabled_languages.join(', ')}...")
130
+ v.save!
131
+ end
132
+ true
133
+ end
134
+
135
+ # Loads the metadata files and stores them into the options object
136
+ def load_from_filesystem(options)
137
+ return if options[:skip_metadata]
138
+
139
+ # Load localised data
140
+ Loader.language_folders(options[:metadata_path]).each do |lng_folder|
141
+ language = File.basename(lng_folder)
142
+ (LOCALISED_VERSION_VALUES + LOCALISED_APP_VALUES).each do |key|
143
+ path = File.join(lng_folder, "#{key}.txt")
144
+ next unless File.exist?(path)
145
+
146
+ UI.message("Loading '#{path}'...")
147
+ options[key] ||= {}
148
+ options[key][language] ||= File.read(path)
149
+ end
150
+ end
151
+
152
+ # Load non localised data
153
+ (NON_LOCALISED_VERSION_VALUES + NON_LOCALISED_APP_VALUES).each do |key|
154
+ path = File.join(options[:metadata_path], "#{key}.txt")
155
+ next unless File.exist?(path)
156
+
157
+ UI.message("Loading '#{path}'...")
158
+ options[key] ||= File.read(path)
159
+ end
160
+ end
161
+
162
+ private
163
+
164
+ def set_review_information(v, options)
165
+ return unless options[:app_review_information]
166
+ info = options[:app_review_information]
167
+ UI.user_error!('`app_review_information` must be a hash') unless info.is_a?(Hash)
168
+
169
+ v.review_first_name = info[:first_name] if info[:first_name]
170
+ v.review_last_name = info[:last_name] if info[:last_name]
171
+ v.review_phone_number = info[:phone_number] if info[:phone_number]
172
+ v.review_email = info[:email_address] if info[:email_address]
173
+ v.review_demo_user = info[:demo_user] if info[:demo_user]
174
+ v.review_demo_password = info[:demo_password] if info[:demo_password]
175
+ v.review_user_needed = (v.review_demo_user.to_s + v.review_demo_password.to_s).length > 0
176
+ v.review_notes = info[:notes] if info[:notes]
177
+ end
178
+
179
+ def set_app_rating(v, options)
180
+ return unless options[:app_rating_config_path]
181
+
182
+ require 'json'
183
+ begin
184
+ json = JSON.parse(File.read(options[:app_rating_config_path]))
185
+ rescue => ex
186
+ UI.error(ex.to_s)
187
+ UI.user_error!("Error parsing JSON file at path '#{options[:app_rating_config_path]}'")
188
+ end
189
+ v.update_rating(json)
190
+ end
191
+ end
192
+ end