produce 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 1f1298d8993f1a0bfa7ca5a98349f8c43f428ee6
4
+ data.tar.gz: d1229985cfdc3dd717655096b6394525f33981d8
5
+ SHA512:
6
+ metadata.gz: ac92d64e9e110bb5b84dff7c25e7a66edcf69c9b071386b54f745298a1a41083a8da75a0d403dfe7f21e053a706b1d0cc6e358449723f1c15844c73f4dee6190
7
+ data.tar.gz: 40233a20a2eb1c4a4e8c616c250274c488ee9192b9872d7ec0f1e7961bfea87ede47136ac55d1c425cb78a6d9f44bee23b0984a6e26e64a661f21846810d77f7
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015 Felix Krause
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,142 @@
1
+ <h3 align="center">
2
+ <a href="https://github.com/KrauseFx/fastlane">
3
+ <img src="assets/fastlane.png" width="150" />
4
+ <br />
5
+ fastlane
6
+ </a>
7
+ </h3>
8
+ <p align="center">
9
+ <a href="https://github.com/KrauseFx/deliver">deliver</a> &bull;
10
+ <a href="https://github.com/KrauseFx/snapshot">snapshot</a> &bull;
11
+ <a href="https://github.com/KrauseFx/frameit">frameit</a> &bull;
12
+ <a href="https://github.com/KrauseFx/PEM">PEM</a> &bull;
13
+ <a href="https://github.com/KrauseFx/sigh">sigh</a> &bull;
14
+ <b>produce</b>
15
+ </p>
16
+ -------
17
+
18
+ <p align="center">
19
+ <img src="assets/produce.png">
20
+ </p>
21
+
22
+ produce
23
+ ============
24
+
25
+ [![Twitter: @KauseFx](https://img.shields.io/badge/contact-@KrauseFx-blue.svg?style=flat)](https://twitter.com/KrauseFx)
26
+ [![License](http://img.shields.io/badge/license-MIT-green.svg?style=flat)](https://github.com/KrauseFx/produce/blob/master/LICENSE)
27
+ [![Gem](https://img.shields.io/gem/v/produce.svg?style=flat)](http://rubygems.org/gems/produce)
28
+
29
+ ###### Create new iOS apps on iTunes Connect and Dev Portal using your command line
30
+
31
+ ##### This tool was sponsored by [AppInstitute](http://appinstitute.co.uk/).
32
+
33
+ Get in contact with the developer on Twitter: [@KrauseFx](https://twitter.com/KrauseFx)
34
+
35
+
36
+
37
+ -------
38
+ <p align="center">
39
+ <a href="#features">Features</a> &bull;
40
+ <a href="#installation">Installation</a> &bull;
41
+ <a href="#usage">Usage</a> &bull;
42
+ <a href="#how-does-it-work">How does it work?</a> &bull;
43
+ <a href="#tips">Tips</a> &bull;
44
+ <a href="#need-help">Need help?</a>
45
+ </p>
46
+
47
+ -------
48
+
49
+ <h5 align="center"><code>produce</code> is part of <a href="http://fastlane.tools">fastlane</a>: connect all deployment tools into one streamlined workflow.</h5>
50
+
51
+
52
+ # Features
53
+
54
+ - **Create** new apps on both iTunes Connect and the Apple Developer Portal
55
+ - Support for **multiple Apple accounts**, storing your credentials securely in the Keychain
56
+
57
+ # Installation
58
+ sudo gem install produce
59
+
60
+ Make sure, you have the latest version of the Xcode command line tools installed:
61
+
62
+ xcode-select --install
63
+
64
+ Install phantomjs (this is needed to control the Apple Developer Portal)
65
+
66
+ brew update && brew install phantomjs
67
+
68
+ If you don't already have homebrew installed, [install it here](http://brew.sh/).
69
+
70
+ # Usage
71
+
72
+ produce
73
+
74
+ ## Environment Variables
75
+ In case you want to pass more information to `produce`:
76
+
77
+ - `PRODUCE_USERNAME` (your iTunes Connect username)
78
+ - `PRODUCE_APP_IDENTIFIER` (the bundle identifier of the new app)
79
+ - `PRODUCE_APP_NAME` (the name of the new app)
80
+ - `PRODUCE_LANGUAGE` (the language you want your app to use, e.g. `English`, `German`)
81
+ - `PRODUCE_VERSION` (the initial app version)
82
+ - `PRODUCE_SKU` (the SKU you want to use, which must be a unique number)
83
+ - `PRODUCE_TEAM_ID` (the Team ID, e.g. `Q2CBPK58CA`)
84
+ - `PRODUCE_TEAM_NAME` (the Team Name, e.g. `Felix Krause`)
85
+
86
+ ## `fastlane` Integration
87
+
88
+ Your `Fastfile`
89
+ ```ruby
90
+ lane :appstore do
91
+ produce({
92
+ ...
93
+ })
94
+
95
+ deliver
96
+ end
97
+ ```
98
+
99
+ To use the newly generated app in `deliver`, you have to adapt your `Deliverfile`:
100
+
101
+ ```ruby
102
+ apple_id ENV['PRODUCE_APPLE_ID']
103
+ ```
104
+
105
+ This will tell `deliver`, which `App ID` to use, since the app is not yet available in the App Store.
106
+
107
+ # How does it work?
108
+
109
+ ```produce``` will access the ```iOS Dev Center``` to create your `App ID`. Check out the full source code: [developer_center.rb](https://github.com/KrauseFx/produce/blob/master/lib/produce/developer_center.rb).
110
+
111
+ After finishing the first step, `produce` will access `iTunes Connect` to create the new app with some initial values. Check out the full source code: [itunes_connect.rb](https://github.com/KrauseFx/produce/blob/master/lib/produce/itunes_connect.rb).
112
+
113
+ You'll still have to fill out the remaining information (like screenshots, app description and pricing). You can use [deliver](https://github.com/KrauseFx/deliver) to upload your app metadata using a CLI
114
+
115
+ ## How is my password stored?
116
+ ```produce``` uses the [password manager](https://github.com/KrauseFx/CredentialsManager) from `fastlane`. Take a look the [CredentialsManager README](https://github.com/KrauseFx/CredentialsManager) for more information.
117
+
118
+ # Tips
119
+ ## [`fastlane`](http://fastlane.tools) Toolchain
120
+
121
+ - [`fastlane`](http://fastlane.tools): Connect all deployment tools into one streamlined workflow
122
+ - [`deliver`](https://github.com/KrauseFx/deliver): Upload screenshots, metadata and your app to the App Store using a single command
123
+ - [`snapshot`](https://github.com/KrauseFx/snapshot): Automate taking localized screenshots of your iOS app on every device
124
+ - [`frameit`](https://github.com/KrauseFx/frameit): Quickly put your screenshots into the right device frames
125
+ - [`PEM`](https://github.com/KrauseFx/pem): Automatically generate and renew your push notification profiles
126
+ - [`sigh`](https://github.com/KrauseFx/sigh): Because you would rather spend your time building stuff than fighting provisioning
127
+
128
+ # Need help?
129
+ - If there is a technical problem with `produce`, submit an issue.
130
+ - I'm available for contract work - drop me an email: produce@krausefx.com
131
+
132
+ # License
133
+ This project is licensed under the terms of the MIT license. See the LICENSE file.
134
+
135
+ # Contributing
136
+
137
+ 1. Create an issue to start a discussion about your idea
138
+ 2. Fork it (https://github.com/KrauseFx/produce/fork)
139
+ 3. Create your feature branch (`git checkout -b my-new-feature`)
140
+ 4. Commit your changes (`git commit -am 'Add some feature'`)
141
+ 5. Push to the branch (`git push origin my-new-feature`)
142
+ 6. Create a new Pull Request
data/bin/produce ADDED
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $:.push File.expand_path("../../lib", __FILE__)
4
+
5
+ require 'produce'
6
+ require 'commander'
7
+ require 'credentials_manager/password_manager'
8
+ require 'credentials_manager/appfile_config'
9
+
10
+ HighLine.track_eof = false
11
+
12
+ class FastlaneApplication
13
+ include Commander::Methods
14
+
15
+ def run
16
+ program :version, Produce::VERSION
17
+ program :description, 'CLI for \'produce\''
18
+ program :help, 'Author', 'Felix Krause <produce@krausefx.com>'
19
+ program :help, 'Website', 'http://fastlane.tools'
20
+ program :help, 'GitHub', 'https://github.com/krausefx/produce'
21
+ program :help_formatter, :compact
22
+
23
+ always_trace!
24
+
25
+ global_option('-u', '--username STRING', 'Your Apple ID username')
26
+
27
+ command :create do |c|
28
+ c.syntax = 'produce create'
29
+ c.description = 'Creates a new app on iTunes Connect and the Apple Developer Portal'
30
+
31
+ c.action do |args, options|
32
+ set_username(options.username)
33
+
34
+ Produce::Config.shared_config # to ask for missing information right in the beginning
35
+ puts Produce::Manager.start_producing
36
+ end
37
+ end
38
+
39
+ def set_username(username)
40
+ user = username
41
+ user ||= ENV["PRODUCE_USERNAME"]
42
+ user ||= CredentialsManager::AppfileConfig.try_fetch_value(:apple_id)
43
+ CredentialsManager::PasswordManager.shared_manager(user) if user
44
+ end
45
+
46
+ default_command :create
47
+
48
+ run!
49
+ end
50
+ end
51
+
52
+ FastlaneApplication.new.run
@@ -0,0 +1,35 @@
1
+ module Produce
2
+ class AvailableDefaultLanguages
3
+ def self.all_langauges
4
+ [
5
+ "Australian English",
6
+ "Brazilian Portuguese",
7
+ "Canadian English",
8
+ "Canadian French",
9
+ "Danish",
10
+ "Dutch",
11
+ "English",
12
+ "Finnish",
13
+ "French",
14
+ "German",
15
+ "Greek",
16
+ "Indonesian",
17
+ "Italian",
18
+ "Japanese",
19
+ "Korean",
20
+ "Malay",
21
+ "Mexican Spanish",
22
+ "Norwegian",
23
+ "Portuguese",
24
+ "Russian",
25
+ "Simplified Chinese",
26
+ "Spanish",
27
+ "Swedish",
28
+ "Thai",
29
+ "Traditional Chinese",
30
+ "Turkish",
31
+ "UK English"
32
+ ]
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,41 @@
1
+ module Produce
2
+ class Config
3
+ attr_accessor :config
4
+
5
+ def self.shared_config
6
+ @@shared ||= self.new
7
+ end
8
+
9
+ def initialize
10
+ @config = {
11
+ :bundle_identifier => ENV['PRODUCE_APP_IDENTIFIER'],
12
+ :app_name => ENV['PRODUCE_APP_NAME'],
13
+ :primary_language => ENV['PRODUCE_LANGUAGE'],
14
+ :version => ENV['PRODUCE_VERSION'],
15
+ :sku => ENV['PRODUCE_SKU']
16
+ }
17
+
18
+ @config[:bundle_identifier] ||= CredentialsManager::AppfileConfig.try_fetch_value(:app_identifier)
19
+ @config[:bundle_identifier] ||= ask("App Identifier (Bundle ID, e.g. com.krausefx.app): ")
20
+ @config[:app_name] ||= ask("App Name: ")
21
+
22
+ while @config[:primary_language].to_s.length == 0
23
+ input = ask("Primary Language (e.g. 'English', 'German'): ")
24
+ input = input.split.map(&:capitalize).join(' ')
25
+ if not AvailableDefaultLanguages.all_langauges.include?(input)
26
+ Helper.log.error "Could not find langauge #{input} - available languages: #{AvailableDefaultLanguages.all_langauges}"
27
+ else
28
+ @config[:primary_language] = input
29
+ end
30
+ end
31
+
32
+ @config[:version] ||= ask("Initial version number (e.g. '1.0'): ")
33
+ @config[:sku] ||= ask("SKU Number (e.g. '1234'): ")
34
+ end
35
+
36
+ def self.val(key)
37
+ raise "Please only pass symbols, no Strings to this method".red unless key.kind_of?Symbol
38
+ self.shared_config.config[key]
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,32 @@
1
+ module Produce
2
+ class DependencyChecker
3
+ def self.check_dependencies
4
+ self.check_phantom_js
5
+ self.check_xcode_select unless Helper.is_test?
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 produce"
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 produce again"
16
+ Helper.log.fatal '#############################################################'
17
+ raise "Run 'brew update && brew install phantomjs' and start produce 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 produce"
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 produce again"
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,289 @@
1
+ require 'credentials_manager/password_manager'
2
+ require 'open-uri'
3
+ require 'openssl'
4
+
5
+ require 'capybara'
6
+ require 'capybara/poltergeist'
7
+
8
+ module Produce
9
+ class DeveloperCenter
10
+ # This error occurs only if there is something wrong with the given login data
11
+ class DeveloperCenterLoginError < StandardError
12
+ end
13
+
14
+ # This error can occur for many reaons. It is
15
+ # usually raised when a UI element could not be found
16
+ class DeveloperCenterGeneralError < StandardError
17
+ end
18
+
19
+ # Types of certificates
20
+ APPSTORE = "AppStore"
21
+ ADHOC = "AdHoc"
22
+ DEVELOPMENT = "Development"
23
+
24
+ include Capybara::DSL
25
+
26
+ DEVELOPER_CENTER_URL = "https://developer.apple.com/devcenter/ios/index.action"
27
+ APPS_URL = "https://developer.apple.com/account/ios/identifiers/bundle/bundleList.action"
28
+ CREATE_APP_URL = "https://developer.apple.com/account/ios/identifiers/bundle/bundleCreate.action"
29
+
30
+
31
+
32
+ def initialize
33
+ FileUtils.mkdir_p TMP_FOLDER
34
+
35
+ Capybara.run_server = false
36
+ Capybara.default_driver = :poltergeist
37
+ Capybara.javascript_driver = :poltergeist
38
+ Capybara.current_driver = :poltergeist
39
+ Capybara.app_host = DEVELOPER_CENTER_URL
40
+
41
+ # Since Apple has some SSL errors, we have to configure the client properly:
42
+ # https://github.com/ariya/phantomjs/issues/11239
43
+ Capybara.register_driver :poltergeist do |a|
44
+ conf = ['--debug=no', '--ignore-ssl-errors=yes', '--ssl-protocol=TLSv1']
45
+ Capybara::Poltergeist::Driver.new(a, {
46
+ phantomjs_options: conf,
47
+ phantomjs_logger: File.open("#{TMP_FOLDER}/poltergeist_log.txt", "a"),
48
+ js_errors: false
49
+ })
50
+ end
51
+
52
+ page.driver.headers = { "Accept-Language" => "en" }
53
+
54
+ self.login
55
+ end
56
+
57
+ # Loggs in a user with the given login data on the Dev Center Frontend.
58
+ # You don't need to pass a username and password. It will
59
+ # Automatically be fetched using the {CredentialsManager::PasswordManager}.
60
+ # This method will also automatically be called when triggering other
61
+ # actions like {#open_app_page}
62
+ # @param user (String) (optional) The username/email address
63
+ # @param password (String) (optional) The password
64
+ # @return (bool) true if everything worked fine
65
+ # @raise [DeveloperCenterGeneralError] General error while executing
66
+ # this action
67
+ # @raise [DeveloperCenterLoginError] Login data is wrong
68
+ def login(user = nil, password = nil)
69
+ begin
70
+ Helper.log.info "Login into iOS Developer Center"
71
+
72
+ user ||= CredentialsManager::PasswordManager.shared_manager.username
73
+ password ||= CredentialsManager::PasswordManager.shared_manager.password
74
+
75
+ result = visit APPS_URL
76
+ raise "Could not open Developer Center" unless result['status'] == 'success'
77
+
78
+ if page.has_content?"Member Center"
79
+ # Already logged in
80
+ return true
81
+ end
82
+
83
+ (wait_for_elements(".button.blue").first.click rescue nil) # maybe already logged in
84
+
85
+ (wait_for_elements('#accountpassword') rescue nil) # when the user is already logged in, this will raise an exception
86
+
87
+ if page.has_content?"Member Center"
88
+ # Already logged in
89
+ return true
90
+ end
91
+
92
+ fill_in "accountname", with: user
93
+ fill_in "accountpassword", with: password
94
+
95
+ all(".button.large.blue.signin-button").first.click
96
+
97
+ begin
98
+ if page.has_content?"Select Team" # If the user is not on multiple teams
99
+ select_team
100
+ end
101
+ rescue => ex
102
+ Helper.log.debug ex
103
+ raise DeveloperCenterLoginError.new("Error loggin in user #{user}. User is on multiple teams and we couldn't select the one specified.")
104
+ end
105
+
106
+ begin
107
+ wait_for_elements('.toolbar-button.add.navLink')
108
+ visit APPS_URL # again, since after the login, the dev center loses the GET value
109
+ rescue => ex
110
+ Helper.log.debug ex
111
+ if page.has_content?"Getting Started"
112
+ raise "There was no valid signing certificate found. Please log in and follow the 'Getting Started guide' on '#{current_url}'".red
113
+ else
114
+ raise DeveloperCenterLoginError.new("Error logging in user #{user} with the given password. Make sure you entered them correctly.")
115
+ end
116
+ end
117
+
118
+
119
+ Helper.log.info "Login successful"
120
+
121
+ true
122
+ rescue => ex
123
+ error_occured(ex)
124
+ end
125
+ end
126
+
127
+ def select_team
128
+ team_id = ENV["PRODUCE_TEAM_ID"]
129
+ team_id = nil if team_id.to_s.length == 0
130
+
131
+ team_name = ENV["PRODUCE_TEAM_NAME"]
132
+ team_name = nil if team_name.to_s.length == 0
133
+
134
+ if team_id == nil and team_name == nil
135
+ Helper.log.info "You can store you preferred team using the environment variable `PRODUCE_TEAM_ID` or `PRODUCE_TEAM_NAME`".green
136
+ Helper.log.info "Your ID belongs to the following teams:".green
137
+ end
138
+
139
+ available_options = []
140
+
141
+ teams = find("div.input").all('.team-value') # Grab all the teams data
142
+ teams.each_with_index do |val, index|
143
+ current_team_id = '"' + val.find("input").value + '"'
144
+ team_text = val.find(".label-primary").text
145
+ description_text = val.find(".label-secondary").text
146
+ description_text = "(#{description_text})" unless description_text.empty? # Include the team description if any
147
+ index_text = (index + 1).to_s + "."
148
+
149
+ available_options << [index_text, current_team_id, team_text, description_text].join(" ")
150
+ end
151
+
152
+ if team_name
153
+ # Search for name
154
+ found_it = false
155
+ all("label.label-primary").each do |current|
156
+ if current.text.downcase.gsub(/\s+/, "") == team_name.downcase.gsub(/\s+/, "")
157
+ current.click # select the team by name
158
+ found_it = true
159
+ end
160
+ end
161
+
162
+ unless found_it
163
+ available_teams = all("label.label-primary").collect { |a| a.text }
164
+ raise DeveloperCenterLoginError.new("Could not find Team with name '#{team_name}'. Available Teams: #{available_teams}".red)
165
+ end
166
+ else
167
+ # Search by ID/Index
168
+ unless team_id
169
+ puts available_options.join("\n").green
170
+ team_index = ask("Please select the team number you would like to access: ".green)
171
+ team_id = teams[team_index.to_i - 1].find(".radio").value
172
+ end
173
+
174
+ team_button = first(:xpath, "//input[@type='radio' and @value='#{team_id}']") # Select the desired team
175
+ if team_button
176
+ team_button.click
177
+ else
178
+ Helper.log.fatal "Could not find given Team. Available options: ".red
179
+ puts available_options.join("\n").yellow
180
+ raise DeveloperCenterLoginError.new("Error finding given team #{team_id}.".red)
181
+ end
182
+ end
183
+
184
+ all(".button.large.blue.submit").first.click
185
+
186
+ result = visit APPS_URL
187
+ raise "Could not open Developer Center" unless result['status'] == 'success'
188
+ end
189
+
190
+ def run
191
+ create_new_app
192
+ rescue => ex
193
+ error_occured(ex)
194
+ end
195
+
196
+ def create_new_app
197
+ if app_exists?
198
+ Helper.log.info "App '#{Config.val(:app_name)}' already exists, nothing to do on the Dev Center".green
199
+ ENV["CREATED_NEW_APP_ID"] = nil
200
+ # Nothing to do here
201
+ else
202
+ Helper.log.info "Creating new app '#{Config.val(:app_name)}' on the Apple Dev Center".green
203
+ visit CREATE_APP_URL
204
+ wait_for_elements("*[name='appIdName']").first.set Config.val(:app_name)
205
+ wait_for_elements("*[name='explicitIdentifier']").first.set Config.val(:bundle_identifier)
206
+ click_next
207
+
208
+ sleep 3 # sometimes this takes a while and we don't want to timeout
209
+
210
+ wait_for_elements("form[name='bundleSubmit']") # this will show the summary of the given information
211
+ click_next
212
+
213
+ sleep 3 # sometimes this takes a while and we don't want to timeout
214
+
215
+ wait_for_elements(".ios.bundles.confirmForm.complete")
216
+ click_on "Done"
217
+
218
+ raise "Something went wrong when creating the new app - it's not listed in the App's list" unless app_exists?
219
+
220
+ ENV["CREATED_NEW_APP_ID"] = Time.now.to_s
221
+
222
+ Helper.log.info "Finished creating new app '#{Config.val(:app_name)}' on the Dev Center".green
223
+ end
224
+
225
+ return true
226
+ end
227
+
228
+
229
+ private
230
+ def app_exists?
231
+ visit APPS_URL
232
+
233
+ wait_for_elements("td[aria-describedby='grid-table_identifier']").each do |app|
234
+ identifier = app['title']
235
+
236
+ return true if identifier.to_s == Config.val(:bundle_identifier).to_s
237
+ end
238
+
239
+ false
240
+ end
241
+
242
+ def click_next
243
+ wait_for_elements('.button.small.blue.right.submit').last.click
244
+ end
245
+
246
+ def error_occured(ex)
247
+ snap
248
+ raise ex # re-raise the error after saving the snapshot
249
+ end
250
+
251
+ def snap
252
+ path = "Error#{Time.now.to_i}.png"
253
+ save_screenshot(path, :full => true)
254
+ system("open '#{path}'")
255
+ end
256
+
257
+ def wait_for(method, parameter, success)
258
+ counter = 0
259
+ result = method.call(parameter)
260
+ while !success.call(result)
261
+ sleep 0.2
262
+
263
+ result = method.call(parameter)
264
+
265
+ counter += 1
266
+ if counter > 100
267
+ Helper.log.debug caller
268
+ raise DeveloperCenterGeneralError.new("Couldn't find '#{parameter}' after waiting for quite some time")
269
+ end
270
+ end
271
+ return result
272
+ end
273
+
274
+ def wait_for_elements(name)
275
+ method = Proc.new { |n| all(name) }
276
+ success = Proc.new { |r| r.count > 0 }
277
+ return wait_for(method, name, success)
278
+ end
279
+
280
+ def wait_for_variable(name)
281
+ method = Proc.new { |n|
282
+ retval = page.html.match(/var #{n} = "(.*)"/)
283
+ retval[1] unless retval == nil
284
+ }
285
+ success = Proc.new { |r| r != nil }
286
+ return wait_for(method, name, success)
287
+ end
288
+ end
289
+ end
@@ -0,0 +1,49 @@
1
+ require 'logger'
2
+
3
+ module Produce
4
+ module Helper
5
+
6
+ # Logging happens using this method
7
+ def self.log
8
+ if is_test?
9
+ @@log ||= Logger.new(STDOUT) # 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
+ end
49
+ end
@@ -0,0 +1,232 @@
1
+ require 'capybara'
2
+ require 'capybara/poltergeist'
3
+ require 'credentials_manager/password_manager'
4
+
5
+ module Produce
6
+ # Every method you call here, might take a time
7
+ class ItunesConnect
8
+ # This error occurs only if there is something wrong with the given login data
9
+ class ItunesConnectLoginError < StandardError
10
+ end
11
+
12
+ # This error can occur for many reaons. It is
13
+ # usually raised when a UI element could not be found
14
+ class ItunesConnectGeneralError < StandardError
15
+ end
16
+
17
+ include Capybara::DSL
18
+
19
+ ITUNESCONNECT_URL = "https://itunesconnect.apple.com/"
20
+ APPS_URL = "https://itunesconnect.apple.com/WebObjects/iTunesConnect.woa/ra/ng/app"
21
+
22
+ NEW_APP_CLASS = ".new-button.ng-isolate-scope"
23
+
24
+ def initialize
25
+ super
26
+
27
+ DependencyChecker.check_dependencies
28
+
29
+ Capybara.run_server = false
30
+ Capybara.default_driver = :poltergeist
31
+ Capybara.javascript_driver = :poltergeist
32
+ Capybara.current_driver = :poltergeist
33
+ Capybara.app_host = ITUNESCONNECT_URL
34
+
35
+ # Since Apple has some SSL errors, we have to configure the client properly:
36
+ # https://github.com/ariya/phantomjs/issues/11239
37
+ Capybara.register_driver :poltergeist do |a|
38
+ conf = ['--debug=no', '--ignore-ssl-errors=yes', '--ssl-protocol=TLSv1']
39
+ Capybara::Poltergeist::Driver.new(a, {
40
+ phantomjs_options: conf,
41
+ phantomjs_logger: File.open("/tmp/poltergeist_log.txt", "a"),
42
+ js_errors: false
43
+ })
44
+ end
45
+
46
+ page.driver.headers = { "Accept-Language" => "en" }
47
+
48
+ self.login
49
+ end
50
+
51
+ # Loggs in a user with the given login data on the iTC Frontend.
52
+ # You don't need to pass a username and password. It will
53
+ # Automatically be fetched using the {CredentialsManager::PasswordManager}.
54
+ # This method will also automatically be called when triggering other
55
+ # actions like {#open_app_page}
56
+ # @param user (String) (optional) The username/email address
57
+ # @param password (String) (optional) The password
58
+ # @return (bool) true if everything worked fine
59
+ # @raise [ItunesConnectGeneralError] General error while executing
60
+ # this action
61
+ # @raise [ItunesConnectLoginError] Login data is wrong
62
+ def login(user = nil, password = nil)
63
+ begin
64
+ Helper.log.info "Logging into iTunesConnect"
65
+
66
+ user ||= CredentialsManager::PasswordManager.shared_manager.username
67
+ password ||= CredentialsManager::PasswordManager.shared_manager.password
68
+
69
+ result = visit ITUNESCONNECT_URL
70
+ raise "Could not open iTunesConnect" unless result['status'] == 'success'
71
+
72
+ (wait_for_elements('#accountpassword') rescue nil) # when the user is already logged in, this will raise an exception
73
+
74
+ if page.has_content?"My Apps"
75
+ # Already logged in
76
+ return true
77
+ end
78
+
79
+ fill_in "accountname", with: user
80
+ fill_in "accountpassword", with: password
81
+
82
+ begin
83
+ (wait_for_elements(".enabled").first.click rescue nil) # Login Button
84
+ wait_for_elements('.homepageWrapper.ng-scope')
85
+
86
+ if page.has_content?"My Apps"
87
+ # Everything looks good
88
+ else
89
+ raise ItunesConnectLoginError.new("Looks like your login data was correct, but you do not have access to the apps.")
90
+ end
91
+ rescue => ex
92
+ Helper.log.debug(ex)
93
+ raise ItunesConnectLoginError.new("Error logging in user #{user} with the given password. Make sure you entered them correctly.")
94
+ end
95
+
96
+ Helper.log.info "Successfully logged into iTunesConnect"
97
+
98
+ true
99
+ rescue => ex
100
+ error_occured(ex)
101
+ end
102
+ end
103
+
104
+ def run
105
+ if ENV["CREATED_NEW_APP_ID"].to_i > 0
106
+ # We just created this App ID, this takes about 3 minutes to show up on iTunes Connect
107
+ Helper.log.info "Waiting for 3 minutes to make sure, the App ID is synced to iTunes Connect".yellow
108
+ sleep 180
109
+ unless app_exists?
110
+ Helper.log.info "Couldn't find new app yet, we're waiting for another 2 minutes.".yellow
111
+ sleep 120
112
+ end
113
+ end
114
+
115
+ return create_new_app
116
+ rescue => ex
117
+ error_occured(ex)
118
+ end
119
+
120
+ def create_new_app
121
+ if app_exists?
122
+ Helper.log.info "App '#{Config.val(:app_name)}' exists already, nothing to do on iTunes Connect".green
123
+ # Nothing to do here
124
+ else
125
+ Helper.log.info "Creating new app '#{Config.val(:app_name)}' on iTunes Connect".green
126
+
127
+ initial_create
128
+
129
+ raise "Something went wrong when creating the new app - it's not listed in the App's list" unless app_exists?
130
+
131
+ Helper.log.info "Finished creating new app '#{Config.val(:app_name)}' on iTunes Connect".green
132
+ end
133
+
134
+ return fetch_apple_id
135
+ end
136
+
137
+ def fetch_apple_id
138
+ # First try it using the Apple API
139
+ data = JSON.parse(open("https://itunes.apple.com/lookup?bundleId=#{Config.val(:bundle_identifier)}").read)
140
+
141
+ if data['resultCount'] == 0 or true
142
+ visit current_url
143
+ sleep 10
144
+ first("input[ng-model='searchModel']").set Config.val(:bundle_identifier)
145
+
146
+ if all("div[bo-bind='app.name']").count == 2
147
+ raise "There were multiple results when looking for the new app. This might be due to having same app identifiers included in each other (see generated screenshots)".red
148
+ end
149
+
150
+ app_url = first("a[bo-href='appBundleLink(app.adamId, app.type)']")[:href]
151
+ apple_id = app_url.split('/').last
152
+
153
+ Helper.log.info "Found Apple ID #{apple_id}".green
154
+ return apple_id
155
+ else
156
+ return data['results'].first['trackId'] # already in the store
157
+ end
158
+ end
159
+
160
+ def initial_create
161
+ open_new_app_popup
162
+
163
+ # Fill out the initial information
164
+ wait_for_elements("input[ng-model='createAppDetails.newApp.name.value']").first.set Config.val(:app_name)
165
+ wait_for_elements("input[ng-model='createAppDetails.versionString.value']").first.set Config.val(:version)
166
+ wait_for_elements("input[ng-model='createAppDetails.newApp.vendorId.value']").first.set Config.val(:sku)
167
+
168
+ wait_for_elements("option[value='#{Config.val(:bundle_identifier)}']").first.select_option
169
+ all(:xpath, "//option[text()='#{Config.val(:primary_language)}']").first.select_option
170
+
171
+ click_on "Create"
172
+ sleep 5 # this usually takes some time
173
+
174
+ if all("p[ng-repeat='error in errorText']").count == 1
175
+ raise all("p[ng-repeat='error in errorText']").first.text.to_s.red # an error when creating this app
176
+ end
177
+
178
+ wait_for_elements(".language.hasPopOver") # looking good
179
+
180
+ Helper.log.info "Successfully created new app '#{Config.val(:app_name)}' on iTC. Setting up the initial information now.".green
181
+ end
182
+
183
+ private
184
+ def app_exists?
185
+ open_new_app_popup # to get the dropdown of available app identifier, if it's there, the app was not yet created
186
+
187
+ sleep 4
188
+
189
+ return (all("option[value='#{Config.val(:bundle_identifier)}']").count == 0)
190
+ end
191
+
192
+ def open_new_app_popup
193
+ visit APPS_URL
194
+ sleep 5 # this usually takes some time
195
+
196
+ wait_for_elements(NEW_APP_CLASS).first.click
197
+ wait_for_elements('#new-menu > * > a').first.click # Create a new App
198
+
199
+ sleep 5 # this usually takes some time - this is important
200
+ wait_for_elements("input[ng-model='createAppDetails.newApp.name.value']") # finish loading
201
+ end
202
+
203
+ def error_occured(ex)
204
+ snap
205
+ raise ex # re-raise the error after saving the snapshot
206
+ end
207
+
208
+ def snap
209
+ path = "Error#{Time.now.to_i}.png"
210
+ save_screenshot(path, :full => true)
211
+ system("open '#{path}'")
212
+ end
213
+
214
+ def wait_for_elements(name)
215
+ counter = 0
216
+ results = all(name)
217
+ while results.count == 0
218
+ # Helper.log.debug "Waiting for #{name}"
219
+ sleep 0.2
220
+
221
+ results = all(name)
222
+
223
+ counter += 1
224
+ if counter > 100
225
+ Helper.log.debug caller
226
+ raise ItunesConnectGeneralError.new("Couldn't find element '#{name}' after waiting for quite some time")
227
+ end
228
+ end
229
+ return results
230
+ end
231
+ end
232
+ end
@@ -0,0 +1,8 @@
1
+ module Produce
2
+ class Manager
3
+ def self.start_producing
4
+ DeveloperCenter.new.run
5
+ return ItunesConnect.new.run
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,44 @@
1
+ require 'open-uri'
2
+
3
+ module Produce
4
+ # Verifies, the user runs the latest version of this gem
5
+ class UpdateChecker
6
+ # This method will check if the latest version is installed and show a warning if that's not the case
7
+ def self.verify_latest_version
8
+ if self.update_available?
9
+ v = fetch_latest
10
+ puts '#######################################################################'.green
11
+ puts "# produce #{v} is available.".green
12
+ puts "# It is recommended to use the latest version.".green
13
+ puts "# Update using '(sudo) gem update produce'.".green
14
+ puts "# To see what's new, open https://github.com/KrauseFx/produce/releases.".green
15
+ puts '#######################################################################'.green
16
+ return true
17
+ end
18
+ false
19
+ end
20
+
21
+ # Is a new official release available (this does not include pre-releases)
22
+ def self.update_available?
23
+ begin
24
+ latest = fetch_latest
25
+ if latest and Gem::Version.new(latest) > Gem::Version.new(current_version)
26
+ return true
27
+ end
28
+ rescue => ex
29
+ Helper.log.error("Could not check if 'produce' is up to date.")
30
+ end
31
+ return false
32
+ end
33
+
34
+ # The currently used version of this gem
35
+ def self.current_version
36
+ Sigh::VERSION
37
+ end
38
+
39
+ private
40
+ def self.fetch_latest
41
+ JSON.parse(open("http://rubygems.org/api/v1/gems/produce.json").read)["version"]
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,3 @@
1
+ module Produce
2
+ VERSION = "0.1.0"
3
+ end
data/lib/produce.rb ADDED
@@ -0,0 +1,20 @@
1
+ require 'json'
2
+ require 'produce/version'
3
+ require 'produce/helper'
4
+ require 'produce/config'
5
+ require 'produce/manager'
6
+ require 'produce/dependency_checker'
7
+ require 'produce/developer_center'
8
+ require 'produce/itunes_connect'
9
+ require 'produce/update_checker'
10
+ require 'produce/available_default_languages'
11
+
12
+ # Third Party code
13
+ require 'colored'
14
+
15
+ module Produce
16
+ TMP_FOLDER = "/tmp/produce/"
17
+
18
+ # Produce::UpdateChecker.verify_latest_version
19
+ DependencyChecker.check_dependencies
20
+ end
metadata ADDED
@@ -0,0 +1,257 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: produce
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Felix Krause
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-01-30 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: json
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '>='
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '>='
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: highline
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ~>
32
+ - !ruby/object:Gem::Version
33
+ version: 1.6.21
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ~>
39
+ - !ruby/object:Gem::Version
40
+ version: 1.6.21
41
+ - !ruby/object:Gem::Dependency
42
+ name: colored
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - '>='
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: commander
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
61
+ version: 4.2.0
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ~>
67
+ - !ruby/object:Gem::Version
68
+ version: 4.2.0
69
+ - !ruby/object:Gem::Dependency
70
+ name: credentials_manager
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - '>='
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - '>='
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: capybara
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ~>
88
+ - !ruby/object:Gem::Version
89
+ version: 2.4.3
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ~>
95
+ - !ruby/object:Gem::Version
96
+ version: 2.4.3
97
+ - !ruby/object:Gem::Dependency
98
+ name: poltergeist
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ~>
102
+ - !ruby/object:Gem::Version
103
+ version: 1.5.1
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ~>
109
+ - !ruby/object:Gem::Version
110
+ version: 1.5.1
111
+ - !ruby/object:Gem::Dependency
112
+ name: bundler
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - '>='
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - '>='
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: rake
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - '>='
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - '>='
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ - !ruby/object:Gem::Dependency
140
+ name: rspec
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ~>
144
+ - !ruby/object:Gem::Version
145
+ version: 3.1.0
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ~>
151
+ - !ruby/object:Gem::Version
152
+ version: 3.1.0
153
+ - !ruby/object:Gem::Dependency
154
+ name: pry
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - '>='
158
+ - !ruby/object:Gem::Version
159
+ version: '0'
160
+ type: :development
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - '>='
165
+ - !ruby/object:Gem::Version
166
+ version: '0'
167
+ - !ruby/object:Gem::Dependency
168
+ name: yard
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - ~>
172
+ - !ruby/object:Gem::Version
173
+ version: 0.8.7.4
174
+ type: :development
175
+ prerelease: false
176
+ version_requirements: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - ~>
179
+ - !ruby/object:Gem::Version
180
+ version: 0.8.7.4
181
+ - !ruby/object:Gem::Dependency
182
+ name: webmock
183
+ requirement: !ruby/object:Gem::Requirement
184
+ requirements:
185
+ - - ~>
186
+ - !ruby/object:Gem::Version
187
+ version: 1.19.0
188
+ type: :development
189
+ prerelease: false
190
+ version_requirements: !ruby/object:Gem::Requirement
191
+ requirements:
192
+ - - ~>
193
+ - !ruby/object:Gem::Version
194
+ version: 1.19.0
195
+ - !ruby/object:Gem::Dependency
196
+ name: codeclimate-test-reporter
197
+ requirement: !ruby/object:Gem::Requirement
198
+ requirements:
199
+ - - '>='
200
+ - !ruby/object:Gem::Version
201
+ version: '0'
202
+ type: :development
203
+ prerelease: false
204
+ version_requirements: !ruby/object:Gem::Requirement
205
+ requirements:
206
+ - - '>='
207
+ - !ruby/object:Gem::Version
208
+ version: '0'
209
+ description: Because you would rather spend your time building stuff than fighting
210
+ provisioning
211
+ email:
212
+ - produce@krausefx.com
213
+ executables:
214
+ - produce
215
+ extensions: []
216
+ extra_rdoc_files: []
217
+ files:
218
+ - LICENSE
219
+ - README.md
220
+ - bin/produce
221
+ - lib/produce.rb
222
+ - lib/produce/available_default_languages.rb
223
+ - lib/produce/config.rb
224
+ - lib/produce/dependency_checker.rb
225
+ - lib/produce/developer_center.rb
226
+ - lib/produce/helper.rb
227
+ - lib/produce/itunes_connect.rb
228
+ - lib/produce/manager.rb
229
+ - lib/produce/update_checker.rb
230
+ - lib/produce/version.rb
231
+ homepage: http://fastlane.tools
232
+ licenses:
233
+ - MIT
234
+ metadata: {}
235
+ post_install_message: produce requires phantomjs. Install it using 'brew update &&
236
+ brew install phantomjs'
237
+ rdoc_options: []
238
+ require_paths:
239
+ - lib
240
+ required_ruby_version: !ruby/object:Gem::Requirement
241
+ requirements:
242
+ - - '>='
243
+ - !ruby/object:Gem::Version
244
+ version: 2.0.0
245
+ required_rubygems_version: !ruby/object:Gem::Requirement
246
+ requirements:
247
+ - - '>='
248
+ - !ruby/object:Gem::Version
249
+ version: '0'
250
+ requirements: []
251
+ rubyforge_project:
252
+ rubygems_version: 2.2.2
253
+ signing_key:
254
+ specification_version: 4
255
+ summary: Because you would rather spend your time building stuff than fighting provisioning
256
+ test_files: []
257
+ has_rdoc: