produce 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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: