produce 0.1.3 → 0.1.4
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 +4 -4
- data/bin/produce +0 -1
- data/lib/produce.rb +4 -8
- data/lib/produce/config.rb +42 -34
- data/lib/produce/developer_center.rb +17 -235
- data/lib/produce/itunes_connect.rb +18 -140
- data/lib/produce/manager.rb +6 -3
- data/lib/produce/version.rb +1 -1
- metadata +3 -103
- data/lib/produce/helper.rb +0 -49
- data/lib/produce/update_checker.rb +0 -44
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA1:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 943588dab52ad39c6de765497b027db1de2e10eb
|
|
4
|
+
data.tar.gz: e4682e29209151d0a9b55881632e56f005b75640
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: eabb9f04201e1e47927b7b48e2892235feb68363d0ff89297ab1c12d689e2c30c55f72d71eba4ccb7867bd904c0f7bb9f9f08f9200edd93234b96c13224d5b2c
|
|
7
|
+
data.tar.gz: 6650261476f7584cf3b94f810d6971a586cb08875dee1d60661d1553eaaefe28f49f670a43ded93765755b112e8dbd38ce36180ffa69e8b4714a188cc5ad30ad
|
data/bin/produce
CHANGED
data/lib/produce.rb
CHANGED
|
@@ -1,21 +1,17 @@
|
|
|
1
1
|
require 'json'
|
|
2
2
|
require 'produce/version'
|
|
3
|
-
require 'produce/helper'
|
|
4
3
|
require 'produce/config'
|
|
5
4
|
require 'produce/manager'
|
|
6
5
|
require 'produce/dependency_checker'
|
|
7
6
|
require 'produce/developer_center'
|
|
8
7
|
require 'produce/itunes_connect'
|
|
9
|
-
require 'produce/update_checker'
|
|
10
8
|
require 'produce/available_default_languages'
|
|
11
9
|
|
|
12
|
-
# Third Party code
|
|
13
|
-
require 'phantomjs/poltergeist'
|
|
14
|
-
require 'colored'
|
|
15
|
-
|
|
16
10
|
module Produce
|
|
17
|
-
|
|
11
|
+
Helper = FastlaneCore::Helper # you gotta love Ruby: Helper.* should use the Helper class contained in FastlaneCore
|
|
12
|
+
|
|
13
|
+
ENV['FASTLANE_TEAM_ID'] ||= ENV["PRODUCE_TEAM_ID"]
|
|
18
14
|
|
|
19
|
-
|
|
15
|
+
FastlaneCore::UpdateChecker.verify_latest_version('produce', Produce::VERSION)
|
|
20
16
|
DependencyChecker.check_dependencies
|
|
21
17
|
end
|
data/lib/produce/config.rb
CHANGED
|
@@ -8,17 +8,51 @@ module Produce
|
|
|
8
8
|
primary_language: "Primary Language (e.g. 'English', 'German'): "
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
# Left to prevent fastlane from crashing. Should be removed upon version bump.
|
|
13
12
|
def self.shared_config
|
|
14
|
-
@@shared ||= self.new
|
|
15
13
|
end
|
|
16
14
|
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
# Creates new Config instance using ENV variables.
|
|
16
|
+
# @param options (Hash) (optional) config options hash. If duplicates keys
|
|
17
|
+
# specified by ENV variable, `options` has value will be used.
|
|
18
|
+
# @return (Config) created Config instance
|
|
19
|
+
def initialize(options = {})
|
|
20
|
+
@config = env_options.merge(options)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Retrieves the value for given `key`. If not found, will promt user with
|
|
24
|
+
# `ASK_MESSAGES[key]` till gets valid response. Thus, always returns value.
|
|
25
|
+
# Raises exception if given `key` is not Symbol or unknown.
|
|
26
|
+
def val(key)
|
|
27
|
+
raise "Please only pass symbols, no Strings to this method".red unless key.kind_of? Symbol
|
|
28
|
+
|
|
29
|
+
unless @config.has_key? key
|
|
30
|
+
@config[key] = ask(ASK_MESSAGES[key]) do |q|
|
|
31
|
+
case key
|
|
32
|
+
when :primary_language
|
|
33
|
+
q.validate = lambda { |val| is_valid_language?(val) }
|
|
34
|
+
q.responses[:not_valid] = "Please enter one of available languages: #{AvailableDefaultLanguages.all_langauges}"
|
|
35
|
+
else
|
|
36
|
+
q.validate = lambda { |val| !val.empty? }
|
|
37
|
+
q.responses[:not_valid] = "#{key.to_s.gsub('_', ' ').capitalize} can't be blank"
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
return @config[key]
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Aliases `[key]` to `val(key)` because Ruby can do it.
|
|
46
|
+
alias_method :[], :val
|
|
47
|
+
|
|
48
|
+
# Returns true if option for the given key is present.
|
|
49
|
+
def has_key?(key)
|
|
50
|
+
@config.has_key? key
|
|
19
51
|
end
|
|
20
52
|
|
|
21
|
-
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def env_options
|
|
22
56
|
hash = {
|
|
23
57
|
bundle_identifier: ENV['PRODUCE_APP_IDENTIFIER'],
|
|
24
58
|
app_name: ENV['PRODUCE_APP_NAME'],
|
|
@@ -42,38 +76,12 @@ module Produce
|
|
|
42
76
|
hash
|
|
43
77
|
end
|
|
44
78
|
|
|
45
|
-
def initialize(options = {})
|
|
46
|
-
@config = Config.env_options.merge(options)
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
def self.val(key)
|
|
50
|
-
raise "Please only pass symbols, no Strings to this method".red unless key.kind_of? Symbol
|
|
51
|
-
|
|
52
|
-
unless shared_config.config.has_key? key
|
|
53
|
-
shared_config.config[key] = ask(ASK_MESSAGES[key]) do |q|
|
|
54
|
-
case key
|
|
55
|
-
when :primary_language
|
|
56
|
-
q.validate = lambda { |val| is_valid_language?(val) }
|
|
57
|
-
q.responses[:not_valid] = "Please enter one of available languages: #{AvailableDefaultLanguages.all_langauges}"
|
|
58
|
-
else
|
|
59
|
-
q.validate = lambda { |val| !val.empty? }
|
|
60
|
-
q.responses[:not_valid] = "#{key.to_s.gsub('_', ' ').capitalize} can't be blank"
|
|
61
|
-
end
|
|
62
|
-
end
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
return self.shared_config.config[key]
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
def self.has_key?(key)
|
|
69
|
-
shared_config.config.has_key? key
|
|
70
|
-
end
|
|
71
79
|
|
|
72
|
-
def
|
|
80
|
+
def is_valid_language? language
|
|
73
81
|
AvailableDefaultLanguages.all_langauges.include? language
|
|
74
82
|
end
|
|
75
83
|
|
|
76
|
-
def
|
|
84
|
+
def skip_itc? value
|
|
77
85
|
%w( true t 1 yes y ).include? value.to_s.downcase
|
|
78
86
|
end
|
|
79
87
|
end
|
|
@@ -1,192 +1,12 @@
|
|
|
1
|
-
require '
|
|
2
|
-
require 'open-uri'
|
|
3
|
-
require 'openssl'
|
|
1
|
+
require 'fastlane_core/developer_center/developer_center'
|
|
4
2
|
|
|
5
|
-
|
|
6
|
-
require 'capybara/poltergeist'
|
|
7
|
-
require 'phantomjs/poltergeist'
|
|
8
|
-
|
|
9
|
-
module Produce
|
|
3
|
+
module FastlaneCore
|
|
10
4
|
class DeveloperCenter
|
|
11
|
-
# This error occurs only if there is something wrong with the given login data
|
|
12
|
-
class DeveloperCenterLoginError < StandardError
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
# This error can occur for many reaons. It is
|
|
16
|
-
# usually raised when a UI element could not be found
|
|
17
|
-
class DeveloperCenterGeneralError < StandardError
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
# Types of certificates
|
|
21
|
-
APPSTORE = "AppStore"
|
|
22
|
-
ADHOC = "AdHoc"
|
|
23
|
-
DEVELOPMENT = "Development"
|
|
24
|
-
|
|
25
|
-
include Capybara::DSL
|
|
26
|
-
|
|
27
|
-
DEVELOPER_CENTER_URL = "https://developer.apple.com/devcenter/ios/index.action"
|
|
28
5
|
APPS_URL = "https://developer.apple.com/account/ios/identifiers/bundle/bundleList.action"
|
|
29
6
|
CREATE_APP_URL = "https://developer.apple.com/account/ios/identifiers/bundle/bundleCreate.action"
|
|
30
7
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
def initialize
|
|
34
|
-
FileUtils.mkdir_p TMP_FOLDER
|
|
35
|
-
|
|
36
|
-
Capybara.run_server = false
|
|
37
|
-
Capybara.default_driver = :poltergeist
|
|
38
|
-
Capybara.javascript_driver = :poltergeist
|
|
39
|
-
Capybara.current_driver = :poltergeist
|
|
40
|
-
Capybara.app_host = DEVELOPER_CENTER_URL
|
|
41
|
-
|
|
42
|
-
# Since Apple has some SSL errors, we have to configure the client properly:
|
|
43
|
-
# https://github.com/ariya/phantomjs/issues/11239
|
|
44
|
-
Capybara.register_driver :poltergeist do |a|
|
|
45
|
-
conf = ['--debug=no', '--ignore-ssl-errors=yes', '--ssl-protocol=TLSv1']
|
|
46
|
-
Capybara::Poltergeist::Driver.new(a, {
|
|
47
|
-
phantomjs: Phantomjs.path,
|
|
48
|
-
phantomjs_options: conf,
|
|
49
|
-
phantomjs_logger: File.open("#{TMP_FOLDER}/poltergeist_log.txt", "a"),
|
|
50
|
-
js_errors: false
|
|
51
|
-
})
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
page.driver.headers = { "Accept-Language" => "en" }
|
|
55
|
-
|
|
56
|
-
self.login
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
# Loggs in a user with the given login data on the Dev Center Frontend.
|
|
60
|
-
# You don't need to pass a username and password. It will
|
|
61
|
-
# Automatically be fetched using the {CredentialsManager::PasswordManager}.
|
|
62
|
-
# This method will also automatically be called when triggering other
|
|
63
|
-
# actions like {#open_app_page}
|
|
64
|
-
# @param user (String) (optional) The username/email address
|
|
65
|
-
# @param password (String) (optional) The password
|
|
66
|
-
# @return (bool) true if everything worked fine
|
|
67
|
-
# @raise [DeveloperCenterGeneralError] General error while executing
|
|
68
|
-
# this action
|
|
69
|
-
# @raise [DeveloperCenterLoginError] Login data is wrong
|
|
70
|
-
def login(user = nil, password = nil)
|
|
71
|
-
begin
|
|
72
|
-
Helper.log.info "Login into iOS Developer Center"
|
|
73
|
-
|
|
74
|
-
user ||= CredentialsManager::PasswordManager.shared_manager.username
|
|
75
|
-
password ||= CredentialsManager::PasswordManager.shared_manager.password
|
|
76
|
-
|
|
77
|
-
result = visit APPS_URL
|
|
78
|
-
raise "Could not open Developer Center" unless result['status'] == 'success'
|
|
79
|
-
|
|
80
|
-
if page.has_content?"Member Center"
|
|
81
|
-
# Already logged in
|
|
82
|
-
return true
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
(wait_for_elements(".button.blue").first.click rescue nil) # maybe already logged in
|
|
86
|
-
|
|
87
|
-
(wait_for_elements('#accountpassword') rescue nil) # when the user is already logged in, this will raise an exception
|
|
88
|
-
|
|
89
|
-
if page.has_content?"Member Center"
|
|
90
|
-
# Already logged in
|
|
91
|
-
return true
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
fill_in "accountname", with: user
|
|
95
|
-
fill_in "accountpassword", with: password
|
|
96
|
-
|
|
97
|
-
all(".button.large.blue.signin-button").first.click
|
|
98
|
-
|
|
99
|
-
begin
|
|
100
|
-
if page.has_content?"Select Team" # If the user is not on multiple teams
|
|
101
|
-
select_team
|
|
102
|
-
end
|
|
103
|
-
rescue => ex
|
|
104
|
-
Helper.log.debug ex
|
|
105
|
-
raise DeveloperCenterLoginError.new("Error loggin in user #{user}. User is on multiple teams and we couldn't select the one specified.")
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
begin
|
|
109
|
-
wait_for_elements('.toolbar-button.add.navLink')
|
|
110
|
-
visit APPS_URL # again, since after the login, the dev center loses the GET value
|
|
111
|
-
rescue => ex
|
|
112
|
-
Helper.log.debug ex
|
|
113
|
-
if page.has_content?"Getting Started"
|
|
114
|
-
raise "There was no valid signing certificate found. Please log in and follow the 'Getting Started guide' on '#{current_url}'".red
|
|
115
|
-
else
|
|
116
|
-
raise DeveloperCenterLoginError.new("Error logging in user #{user} with the given password. Make sure you entered them correctly.")
|
|
117
|
-
end
|
|
118
|
-
end
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
Helper.log.info "Login successful"
|
|
122
|
-
|
|
123
|
-
true
|
|
124
|
-
rescue => ex
|
|
125
|
-
error_occured(ex)
|
|
126
|
-
end
|
|
127
|
-
end
|
|
128
|
-
|
|
129
|
-
def select_team
|
|
130
|
-
team_id = Config.val(:team_id) if Config.has_key?(:team_id)
|
|
131
|
-
team_name = Config.val(:team_name) if Config.has_key?(:team_name)
|
|
132
|
-
|
|
133
|
-
if team_id == nil and team_name == nil
|
|
134
|
-
Helper.log.info "You can store you preferred team using the environment variable `PRODUCE_TEAM_ID` or `PRODUCE_TEAM_NAME`".green
|
|
135
|
-
Helper.log.info "Your ID belongs to the following teams:".green
|
|
136
|
-
end
|
|
137
|
-
|
|
138
|
-
available_options = []
|
|
139
|
-
|
|
140
|
-
teams = find("div.input").all('.team-value') # Grab all the teams data
|
|
141
|
-
teams.each_with_index do |val, index|
|
|
142
|
-
current_team_id = '"' + val.find("input").value + '"'
|
|
143
|
-
team_text = val.find(".label-primary").text
|
|
144
|
-
description_text = val.find(".label-secondary").text
|
|
145
|
-
description_text = "(#{description_text})" unless description_text.empty? # Include the team description if any
|
|
146
|
-
index_text = (index + 1).to_s + "."
|
|
147
|
-
|
|
148
|
-
available_options << [index_text, current_team_id, team_text, description_text].join(" ")
|
|
149
|
-
end
|
|
150
|
-
|
|
151
|
-
if team_name
|
|
152
|
-
# Search for name
|
|
153
|
-
found_it = false
|
|
154
|
-
all("label.label-primary").each do |current|
|
|
155
|
-
if current.text.downcase.gsub(/\s+/, "") == team_name.downcase.gsub(/\s+/, "")
|
|
156
|
-
current.click # select the team by name
|
|
157
|
-
found_it = true
|
|
158
|
-
end
|
|
159
|
-
end
|
|
160
|
-
|
|
161
|
-
unless found_it
|
|
162
|
-
available_teams = all("label.label-primary").collect { |a| a.text }
|
|
163
|
-
raise DeveloperCenterLoginError.new("Could not find Team with name '#{team_name}'. Available Teams: #{available_teams}".red)
|
|
164
|
-
end
|
|
165
|
-
else
|
|
166
|
-
# Search by ID/Index
|
|
167
|
-
unless team_id
|
|
168
|
-
puts available_options.join("\n").green
|
|
169
|
-
team_index = ask("Please select the team number you would like to access: ".green)
|
|
170
|
-
team_id = teams[team_index.to_i - 1].find(".radio").value
|
|
171
|
-
end
|
|
172
|
-
|
|
173
|
-
team_button = first(:xpath, "//input[@type='radio' and @value='#{team_id}']") # Select the desired team
|
|
174
|
-
if team_button
|
|
175
|
-
team_button.click
|
|
176
|
-
else
|
|
177
|
-
Helper.log.fatal "Could not find given Team. Available options: ".red
|
|
178
|
-
puts available_options.join("\n").yellow
|
|
179
|
-
raise DeveloperCenterLoginError.new("Error finding given team #{team_id}.".red)
|
|
180
|
-
end
|
|
181
|
-
end
|
|
182
|
-
|
|
183
|
-
all(".button.large.blue.submit").first.click
|
|
184
|
-
|
|
185
|
-
result = visit APPS_URL
|
|
186
|
-
raise "Could not open Developer Center" unless result['status'] == 'success'
|
|
187
|
-
end
|
|
188
|
-
|
|
189
|
-
def run
|
|
8
|
+
def run(config)
|
|
9
|
+
@config = config
|
|
190
10
|
create_new_app
|
|
191
11
|
rescue => ex
|
|
192
12
|
error_occured(ex)
|
|
@@ -194,22 +14,27 @@ module Produce
|
|
|
194
14
|
|
|
195
15
|
def create_new_app
|
|
196
16
|
if app_exists?
|
|
197
|
-
Helper.log.info "App '#{
|
|
17
|
+
Helper.log.info "App '#{@config[:app_name]}' already exists, nothing to do on the Dev Center".green
|
|
198
18
|
ENV["CREATED_NEW_APP_ID"] = nil
|
|
199
19
|
# Nothing to do here
|
|
200
20
|
else
|
|
201
|
-
Helper.log.info "Creating new app '#{
|
|
21
|
+
Helper.log.info "Creating new app '#{@config[:app_name]}' on the Apple Dev Center".green
|
|
202
22
|
visit CREATE_APP_URL
|
|
203
|
-
wait_for_elements("*[name='appIdName']").first.set
|
|
204
|
-
wait_for_elements("*[name='explicitIdentifier']").first.set
|
|
23
|
+
wait_for_elements("*[name='appIdName']").first.set @config[:app_name]
|
|
24
|
+
wait_for_elements("*[name='explicitIdentifier']").first.set @config[:bundle_identifier]
|
|
205
25
|
click_next
|
|
206
26
|
|
|
207
|
-
sleep
|
|
27
|
+
sleep 5 # sometimes this takes a while and we don't want to timeout
|
|
28
|
+
|
|
29
|
+
if all(".form-error").count > 0
|
|
30
|
+
error = all(".form-error").collect { |a| a.text }.join("\n")
|
|
31
|
+
raise error.red
|
|
32
|
+
end
|
|
208
33
|
|
|
209
34
|
wait_for_elements("form[name='bundleSubmit']") # this will show the summary of the given information
|
|
210
35
|
click_next
|
|
211
36
|
|
|
212
|
-
sleep
|
|
37
|
+
sleep 5 # sometimes this takes a while and we don't want to timeout
|
|
213
38
|
|
|
214
39
|
wait_for_elements(".ios.bundles.confirmForm.complete")
|
|
215
40
|
click_on "Done"
|
|
@@ -218,7 +43,7 @@ module Produce
|
|
|
218
43
|
|
|
219
44
|
ENV["CREATED_NEW_APP_ID"] = Time.now.to_s
|
|
220
45
|
|
|
221
|
-
Helper.log.info "Finished creating new app '#{
|
|
46
|
+
Helper.log.info "Finished creating new app '#{@config[:app_name]}' on the Dev Center".green
|
|
222
47
|
end
|
|
223
48
|
|
|
224
49
|
return true
|
|
@@ -232,7 +57,7 @@ module Produce
|
|
|
232
57
|
wait_for_elements("td[aria-describedby='grid-table_identifier']").each do |app|
|
|
233
58
|
identifier = app['title']
|
|
234
59
|
|
|
235
|
-
return true if identifier.to_s ==
|
|
60
|
+
return true if identifier.to_s == @config[:bundle_identifier].to_s
|
|
236
61
|
end
|
|
237
62
|
|
|
238
63
|
false
|
|
@@ -241,48 +66,5 @@ module Produce
|
|
|
241
66
|
def click_next
|
|
242
67
|
wait_for_elements('.button.small.blue.right.submit').last.click
|
|
243
68
|
end
|
|
244
|
-
|
|
245
|
-
def error_occured(ex)
|
|
246
|
-
snap
|
|
247
|
-
raise ex # re-raise the error after saving the snapshot
|
|
248
|
-
end
|
|
249
|
-
|
|
250
|
-
def snap
|
|
251
|
-
path = "Error#{Time.now.to_i}.png"
|
|
252
|
-
save_screenshot(path, :full => true)
|
|
253
|
-
system("open '#{path}'")
|
|
254
|
-
end
|
|
255
|
-
|
|
256
|
-
def wait_for(method, parameter, success)
|
|
257
|
-
counter = 0
|
|
258
|
-
result = method.call(parameter)
|
|
259
|
-
while !success.call(result)
|
|
260
|
-
sleep 0.2
|
|
261
|
-
|
|
262
|
-
result = method.call(parameter)
|
|
263
|
-
|
|
264
|
-
counter += 1
|
|
265
|
-
if counter > 100
|
|
266
|
-
Helper.log.debug caller
|
|
267
|
-
raise DeveloperCenterGeneralError.new("Couldn't find '#{parameter}' after waiting for quite some time")
|
|
268
|
-
end
|
|
269
|
-
end
|
|
270
|
-
return result
|
|
271
|
-
end
|
|
272
|
-
|
|
273
|
-
def wait_for_elements(name)
|
|
274
|
-
method = Proc.new { |n| all(name) }
|
|
275
|
-
success = Proc.new { |r| r.count > 0 }
|
|
276
|
-
return wait_for(method, name, success)
|
|
277
|
-
end
|
|
278
|
-
|
|
279
|
-
def wait_for_variable(name)
|
|
280
|
-
method = Proc.new { |n|
|
|
281
|
-
retval = page.html.match(/var #{n} = "(.*)"/)
|
|
282
|
-
retval[1] unless retval == nil
|
|
283
|
-
}
|
|
284
|
-
success = Proc.new { |r| r != nil }
|
|
285
|
-
return wait_for(method, name, success)
|
|
286
|
-
end
|
|
287
69
|
end
|
|
288
70
|
end
|
|
@@ -1,109 +1,16 @@
|
|
|
1
|
-
require '
|
|
2
|
-
require 'capybara/poltergeist'
|
|
3
|
-
require 'credentials_manager/password_manager'
|
|
4
|
-
require 'phantomjs/poltergeist'
|
|
1
|
+
require 'fastlane_core/itunes_connect/itunes_connect'
|
|
5
2
|
|
|
6
|
-
module
|
|
3
|
+
module FastlaneCore
|
|
7
4
|
# Every method you call here, might take a time
|
|
8
5
|
class ItunesConnect
|
|
9
|
-
|
|
10
|
-
class ItunesConnectLoginError < StandardError
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
# This error can occur for many reaons. It is
|
|
14
|
-
# usually raised when a UI element could not be found
|
|
15
|
-
class ItunesConnectGeneralError < StandardError
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
include Capybara::DSL
|
|
19
|
-
|
|
20
|
-
ITUNESCONNECT_URL = "https://itunesconnect.apple.com/"
|
|
6
|
+
|
|
21
7
|
APPS_URL = "https://itunesconnect.apple.com/WebObjects/iTunesConnect.woa/ra/ng/app"
|
|
22
8
|
|
|
23
9
|
NEW_APP_CLASS = ".new-button.ng-isolate-scope"
|
|
24
|
-
|
|
25
|
-
def initialize
|
|
26
|
-
super
|
|
27
10
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
Capybara.run_server = false
|
|
31
|
-
Capybara.default_driver = :poltergeist
|
|
32
|
-
Capybara.javascript_driver = :poltergeist
|
|
33
|
-
Capybara.current_driver = :poltergeist
|
|
34
|
-
Capybara.app_host = ITUNESCONNECT_URL
|
|
35
|
-
|
|
36
|
-
# Since Apple has some SSL errors, we have to configure the client properly:
|
|
37
|
-
# https://github.com/ariya/phantomjs/issues/11239
|
|
38
|
-
Capybara.register_driver :poltergeist do |a|
|
|
39
|
-
conf = ['--debug=no', '--ignore-ssl-errors=yes', '--ssl-protocol=TLSv1']
|
|
40
|
-
Capybara::Poltergeist::Driver.new(a, {
|
|
41
|
-
phantomjs: Phantomjs.path,
|
|
42
|
-
phantomjs_options: conf,
|
|
43
|
-
phantomjs_logger: File.open("/tmp/poltergeist_log.txt", "a"),
|
|
44
|
-
js_errors: false
|
|
45
|
-
})
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
page.driver.headers = { "Accept-Language" => "en" }
|
|
49
|
-
|
|
50
|
-
self.login
|
|
51
|
-
end
|
|
11
|
+
def run(config)
|
|
12
|
+
@config = config
|
|
52
13
|
|
|
53
|
-
# Loggs in a user with the given login data on the iTC Frontend.
|
|
54
|
-
# You don't need to pass a username and password. It will
|
|
55
|
-
# Automatically be fetched using the {CredentialsManager::PasswordManager}.
|
|
56
|
-
# This method will also automatically be called when triggering other
|
|
57
|
-
# actions like {#open_app_page}
|
|
58
|
-
# @param user (String) (optional) The username/email address
|
|
59
|
-
# @param password (String) (optional) The password
|
|
60
|
-
# @return (bool) true if everything worked fine
|
|
61
|
-
# @raise [ItunesConnectGeneralError] General error while executing
|
|
62
|
-
# this action
|
|
63
|
-
# @raise [ItunesConnectLoginError] Login data is wrong
|
|
64
|
-
def login(user = nil, password = nil)
|
|
65
|
-
begin
|
|
66
|
-
Helper.log.info "Logging into iTunesConnect"
|
|
67
|
-
|
|
68
|
-
user ||= CredentialsManager::PasswordManager.shared_manager.username
|
|
69
|
-
password ||= CredentialsManager::PasswordManager.shared_manager.password
|
|
70
|
-
|
|
71
|
-
result = visit ITUNESCONNECT_URL
|
|
72
|
-
raise "Could not open iTunesConnect" unless result['status'] == 'success'
|
|
73
|
-
|
|
74
|
-
(wait_for_elements('#accountpassword') rescue nil) # when the user is already logged in, this will raise an exception
|
|
75
|
-
|
|
76
|
-
if page.has_content?"My Apps"
|
|
77
|
-
# Already logged in
|
|
78
|
-
return true
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
fill_in "accountname", with: user
|
|
82
|
-
fill_in "accountpassword", with: password
|
|
83
|
-
|
|
84
|
-
begin
|
|
85
|
-
(wait_for_elements(".enabled").first.click rescue nil) # Login Button
|
|
86
|
-
wait_for_elements('.homepageWrapper.ng-scope')
|
|
87
|
-
|
|
88
|
-
if page.has_content?"My Apps"
|
|
89
|
-
# Everything looks good
|
|
90
|
-
else
|
|
91
|
-
raise ItunesConnectLoginError.new("Looks like your login data was correct, but you do not have access to the apps.")
|
|
92
|
-
end
|
|
93
|
-
rescue => ex
|
|
94
|
-
Helper.log.debug(ex)
|
|
95
|
-
raise ItunesConnectLoginError.new("Error logging in user #{user} with the given password. Make sure you entered them correctly.")
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
Helper.log.info "Successfully logged into iTunesConnect"
|
|
99
|
-
|
|
100
|
-
true
|
|
101
|
-
rescue => ex
|
|
102
|
-
error_occured(ex)
|
|
103
|
-
end
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
def run
|
|
107
14
|
if ENV["CREATED_NEW_APP_ID"].to_i > 0
|
|
108
15
|
# We just created this App ID, this takes about 3 minutes to show up on iTunes Connect
|
|
109
16
|
Helper.log.info "Waiting for 3 minutes to make sure, the App ID is synced to iTunes Connect".yellow
|
|
@@ -121,10 +28,10 @@ module Produce
|
|
|
121
28
|
|
|
122
29
|
def create_new_app
|
|
123
30
|
if app_exists?
|
|
124
|
-
Helper.log.info "App '#{
|
|
31
|
+
Helper.log.info "App '#{@config[:app_name]}' exists already, nothing to do on iTunes Connect".green
|
|
125
32
|
# Nothing to do here
|
|
126
33
|
else
|
|
127
|
-
Helper.log.info "Creating new app '#{
|
|
34
|
+
Helper.log.info "Creating new app '#{@config[:app_name]}' on iTunes Connect".green
|
|
128
35
|
|
|
129
36
|
initial_create
|
|
130
37
|
|
|
@@ -132,7 +39,7 @@ module Produce
|
|
|
132
39
|
|
|
133
40
|
raise "Something went wrong when creating the new app - it's not listed in the App's list" unless app_exists?
|
|
134
41
|
|
|
135
|
-
Helper.log.info "Finished creating new app '#{
|
|
42
|
+
Helper.log.info "Finished creating new app '#{@config[:app_name]}' on iTunes Connect".green
|
|
136
43
|
end
|
|
137
44
|
|
|
138
45
|
return fetch_apple_id
|
|
@@ -140,12 +47,12 @@ module Produce
|
|
|
140
47
|
|
|
141
48
|
def fetch_apple_id
|
|
142
49
|
# First try it using the Apple API
|
|
143
|
-
data = JSON.parse(open("https://itunes.apple.com/lookup?bundleId=#{
|
|
50
|
+
data = JSON.parse(open("https://itunes.apple.com/lookup?bundleId=#{@config[:bundle_identifier]}").read)
|
|
144
51
|
|
|
145
52
|
if data['resultCount'] == 0 or true
|
|
146
53
|
visit current_url
|
|
147
54
|
sleep 10
|
|
148
|
-
first("input[ng-model='searchModel']").set
|
|
55
|
+
first("input[ng-model='searchModel']").set @config[:bundle_identifier]
|
|
149
56
|
|
|
150
57
|
if all("div[bo-bind='app.name']").count == 2
|
|
151
58
|
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
|
|
@@ -165,12 +72,12 @@ module Produce
|
|
|
165
72
|
open_new_app_popup
|
|
166
73
|
|
|
167
74
|
# Fill out the initial information
|
|
168
|
-
wait_for_elements("input[ng-model='createAppDetails.newApp.name.value']").first.set
|
|
169
|
-
wait_for_elements("input[ng-model='createAppDetails.versionString.value']").first.set
|
|
170
|
-
wait_for_elements("input[ng-model='createAppDetails.newApp.vendorId.value']").first.set
|
|
75
|
+
wait_for_elements("input[ng-model='createAppDetails.newApp.name.value']").first.set @config[:app_name]
|
|
76
|
+
wait_for_elements("input[ng-model='createAppDetails.versionString.value']").first.set @config[:version]
|
|
77
|
+
wait_for_elements("input[ng-model='createAppDetails.newApp.vendorId.value']").first.set @config[:sku]
|
|
171
78
|
|
|
172
|
-
wait_for_elements("option[value='#{
|
|
173
|
-
all(:xpath, "//option[text()='#{
|
|
79
|
+
wait_for_elements("option[value='#{@config[:bundle_identifier]}']").first.select_option
|
|
80
|
+
all(:xpath, "//option[text()='#{@config[:primary_language]}']").first.select_option
|
|
174
81
|
|
|
175
82
|
click_on "Create"
|
|
176
83
|
sleep 5 # this usually takes some time
|
|
@@ -181,7 +88,7 @@ module Produce
|
|
|
181
88
|
|
|
182
89
|
wait_for_elements(".language.hasPopOver") # looking good
|
|
183
90
|
|
|
184
|
-
Helper.log.info "Successfully created new app '#{
|
|
91
|
+
Helper.log.info "Successfully created new app '#{@config[:app_name]}' on iTC. Setting up the initial information now.".green
|
|
185
92
|
end
|
|
186
93
|
|
|
187
94
|
def initial_pricing
|
|
@@ -197,12 +104,12 @@ module Produce
|
|
|
197
104
|
|
|
198
105
|
sleep 4
|
|
199
106
|
|
|
200
|
-
return (all("option[value='#{
|
|
107
|
+
return (all("option[value='#{@config[:bundle_identifier]}']").count == 0)
|
|
201
108
|
end
|
|
202
109
|
|
|
203
110
|
def open_new_app_popup
|
|
204
111
|
visit APPS_URL
|
|
205
|
-
sleep
|
|
112
|
+
sleep 8 # this usually takes some time
|
|
206
113
|
|
|
207
114
|
wait_for_elements(NEW_APP_CLASS).first.click
|
|
208
115
|
wait_for_elements('#new-menu > * > a').first.click # Create a new App
|
|
@@ -210,34 +117,5 @@ module Produce
|
|
|
210
117
|
sleep 5 # this usually takes some time - this is important
|
|
211
118
|
wait_for_elements("input[ng-model='createAppDetails.newApp.name.value']") # finish loading
|
|
212
119
|
end
|
|
213
|
-
|
|
214
|
-
def error_occured(ex)
|
|
215
|
-
snap
|
|
216
|
-
raise ex # re-raise the error after saving the snapshot
|
|
217
|
-
end
|
|
218
|
-
|
|
219
|
-
def snap
|
|
220
|
-
path = "Error#{Time.now.to_i}.png"
|
|
221
|
-
save_screenshot(path, :full => true)
|
|
222
|
-
system("open '#{path}'")
|
|
223
|
-
end
|
|
224
|
-
|
|
225
|
-
def wait_for_elements(name)
|
|
226
|
-
counter = 0
|
|
227
|
-
results = all(name)
|
|
228
|
-
while results.count == 0
|
|
229
|
-
# Helper.log.debug "Waiting for #{name}"
|
|
230
|
-
sleep 0.2
|
|
231
|
-
|
|
232
|
-
results = all(name)
|
|
233
|
-
|
|
234
|
-
counter += 1
|
|
235
|
-
if counter > 100
|
|
236
|
-
Helper.log.debug caller
|
|
237
|
-
raise ItunesConnectGeneralError.new("Couldn't find element '#{name}' after waiting for quite some time")
|
|
238
|
-
end
|
|
239
|
-
end
|
|
240
|
-
return results
|
|
241
|
-
end
|
|
242
120
|
end
|
|
243
121
|
end
|
data/lib/produce/manager.rb
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
module Produce
|
|
2
2
|
class Manager
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
# Produces app at DeveloperCenter and ItunesConnect
|
|
4
|
+
# @param config (Config) (optional) config to use. Will fallback to
|
|
5
|
+
# config with ENV values if not specified.
|
|
6
|
+
def self.start_producing(config = Config.new)
|
|
7
|
+
FastlaneCore::DeveloperCenter.new.run(config)
|
|
8
|
+
return FastlaneCore::ItunesConnect.new.run(config) unless config[:skip_itc]
|
|
6
9
|
end
|
|
7
10
|
end
|
|
8
11
|
end
|
data/lib/produce/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: produce
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Felix Krause
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2015-02-
|
|
11
|
+
date: 2015-02-18 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
|
-
name:
|
|
14
|
+
name: fastlane_core
|
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
|
16
16
|
requirements:
|
|
17
17
|
- - '>='
|
|
@@ -24,104 +24,6 @@ dependencies:
|
|
|
24
24
|
- - '>='
|
|
25
25
|
- !ruby/object:Gem::Version
|
|
26
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: phantomjs
|
|
85
|
-
requirement: !ruby/object:Gem::Requirement
|
|
86
|
-
requirements:
|
|
87
|
-
- - ~>
|
|
88
|
-
- !ruby/object:Gem::Version
|
|
89
|
-
version: 1.9.8
|
|
90
|
-
type: :runtime
|
|
91
|
-
prerelease: false
|
|
92
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
93
|
-
requirements:
|
|
94
|
-
- - ~>
|
|
95
|
-
- !ruby/object:Gem::Version
|
|
96
|
-
version: 1.9.8
|
|
97
|
-
- !ruby/object:Gem::Dependency
|
|
98
|
-
name: capybara
|
|
99
|
-
requirement: !ruby/object:Gem::Requirement
|
|
100
|
-
requirements:
|
|
101
|
-
- - ~>
|
|
102
|
-
- !ruby/object:Gem::Version
|
|
103
|
-
version: 2.4.3
|
|
104
|
-
type: :runtime
|
|
105
|
-
prerelease: false
|
|
106
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
107
|
-
requirements:
|
|
108
|
-
- - ~>
|
|
109
|
-
- !ruby/object:Gem::Version
|
|
110
|
-
version: 2.4.3
|
|
111
|
-
- !ruby/object:Gem::Dependency
|
|
112
|
-
name: poltergeist
|
|
113
|
-
requirement: !ruby/object:Gem::Requirement
|
|
114
|
-
requirements:
|
|
115
|
-
- - ~>
|
|
116
|
-
- !ruby/object:Gem::Version
|
|
117
|
-
version: 1.5.1
|
|
118
|
-
type: :runtime
|
|
119
|
-
prerelease: false
|
|
120
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
121
|
-
requirements:
|
|
122
|
-
- - ~>
|
|
123
|
-
- !ruby/object:Gem::Version
|
|
124
|
-
version: 1.5.1
|
|
125
27
|
- !ruby/object:Gem::Dependency
|
|
126
28
|
name: bundler
|
|
127
29
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -237,10 +139,8 @@ files:
|
|
|
237
139
|
- lib/produce/config.rb
|
|
238
140
|
- lib/produce/dependency_checker.rb
|
|
239
141
|
- lib/produce/developer_center.rb
|
|
240
|
-
- lib/produce/helper.rb
|
|
241
142
|
- lib/produce/itunes_connect.rb
|
|
242
143
|
- lib/produce/manager.rb
|
|
243
|
-
- lib/produce/update_checker.rb
|
|
244
144
|
- lib/produce/version.rb
|
|
245
145
|
homepage: http://fastlane.tools
|
|
246
146
|
licenses:
|
data/lib/produce/helper.rb
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
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
|
|
@@ -1,44 +0,0 @@
|
|
|
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
|
-
Produce::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
|