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 +7 -0
- data/LICENSE +21 -0
- data/README.md +142 -0
- data/bin/produce +52 -0
- data/lib/produce/available_default_languages.rb +35 -0
- data/lib/produce/config.rb +41 -0
- data/lib/produce/dependency_checker.rb +32 -0
- data/lib/produce/developer_center.rb +289 -0
- data/lib/produce/helper.rb +49 -0
- data/lib/produce/itunes_connect.rb +232 -0
- data/lib/produce/manager.rb +8 -0
- data/lib/produce/update_checker.rb +44 -0
- data/lib/produce/version.rb +3 -0
- data/lib/produce.rb +20 -0
- metadata +257 -0
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> •
|
10
|
+
<a href="https://github.com/KrauseFx/snapshot">snapshot</a> •
|
11
|
+
<a href="https://github.com/KrauseFx/frameit">frameit</a> •
|
12
|
+
<a href="https://github.com/KrauseFx/PEM">PEM</a> •
|
13
|
+
<a href="https://github.com/KrauseFx/sigh">sigh</a> •
|
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
|
+
[](https://twitter.com/KrauseFx)
|
26
|
+
[](https://github.com/KrauseFx/produce/blob/master/LICENSE)
|
27
|
+
[](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> •
|
40
|
+
<a href="#installation">Installation</a> •
|
41
|
+
<a href="#usage">Usage</a> •
|
42
|
+
<a href="#how-does-it-work">How does it work?</a> •
|
43
|
+
<a href="#tips">Tips</a> •
|
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,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
|
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:
|