applebot 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +38 -0
- data/LICENSE +19 -0
- data/README.md +153 -0
- data/Rakefile +13 -0
- data/applebot.gemspec +27 -0
- data/bin/applebot +73 -0
- data/create_app_id_manifest.js +6 -0
- data/create_manifest.js +24 -0
- data/lib/applebot/command_proxy.rb +44 -0
- data/lib/applebot/commands.rb +74 -0
- data/lib/applebot/error.rb +97 -0
- data/lib/applebot/mkmf.rb +25 -0
- data/lib/applebot/shell.rb +79 -0
- data/lib/applebot/version.rb +5 -0
- data/lib/applebot.rb +109 -0
- data/phantom/_commands.json +318 -0
- data/phantom/applebot.js +656 -0
- data/phantom/create_app.js +355 -0
- data/phantom/create_app_id.js +80 -0
- data/phantom/create_profile.js +134 -0
- data/phantom/delete_app_id.js +57 -0
- data/phantom/download_profile.js +36 -0
- data/phantom/list_app_id.js +66 -0
- data/phantom/list_profile.js +23 -0
- data/phantom/reject_binary_app.js +73 -0
- data/phantom/remove_from_sale_app.js +50 -0
- data/phantom/update_app.js +309 -0
- data/update_app_manifest.js +9 -0
- metadata +143 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 8abd1f581c359df890dca0e9ed9a458d6244a41d
|
4
|
+
data.tar.gz: 1af37a02a80c482bc8da15de976853215d4f203b
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: e5eb30d8e6205b5d25347ea4c16953d6540ba276fa92ccab84ea659d019f2dfd3386c7cf06fddb461b98a2f5dac4c4bcbf3a1b2b6e7a12d5c837433135d3b41b
|
7
|
+
data.tar.gz: 8078823f751ddf30c2ffc578fd3273c151c9e01a735e8f1f719ada1dd7117e4ba0808436475dd3fc7721e58752fc62e6e41a047f8247bec527a6155f8437a9c1
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
applebot (0.0.1)
|
5
|
+
activesupport (>= 3.2)
|
6
|
+
commander (~> 4.1.2)
|
7
|
+
terminal-table (>= 1.4.0)
|
8
|
+
|
9
|
+
GEM
|
10
|
+
remote: https://rubygems.org/
|
11
|
+
specs:
|
12
|
+
activesupport (4.0.3)
|
13
|
+
i18n (~> 0.6, >= 0.6.4)
|
14
|
+
minitest (~> 4.2)
|
15
|
+
multi_json (~> 1.3)
|
16
|
+
thread_safe (~> 0.1)
|
17
|
+
tzinfo (~> 0.3.37)
|
18
|
+
atomic (1.1.14)
|
19
|
+
commander (4.1.6)
|
20
|
+
highline (~> 1.6.11)
|
21
|
+
dotenv (0.10.0)
|
22
|
+
highline (1.6.21)
|
23
|
+
i18n (0.6.9)
|
24
|
+
minitest (4.7.5)
|
25
|
+
multi_json (1.9.3)
|
26
|
+
rake (10.1.0)
|
27
|
+
terminal-table (1.4.5)
|
28
|
+
thread_safe (0.1.3)
|
29
|
+
atomic
|
30
|
+
tzinfo (0.3.38)
|
31
|
+
|
32
|
+
PLATFORMS
|
33
|
+
ruby
|
34
|
+
|
35
|
+
DEPENDENCIES
|
36
|
+
applebot!
|
37
|
+
dotenv
|
38
|
+
rake
|
data/LICENSE
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
Copyright (c) 2013-2014 Turboprop Inc (https://usepropeller.com/)
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
5
|
+
in the Software without restriction, including without limitation the rights
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
8
|
+
furnished to do so, subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included in
|
11
|
+
all copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,153 @@
|
|
1
|
+
# AppleBot - The Apple Robot
|
2
|
+
|
3
|
+
A CLI and Ruby library to manage Apple Developer Center and iTunes Connect tasks. Uses [CasperJS](http://casperjs.org/) for easy maintenance and flexibility.
|
4
|
+
|
5
|
+
## Requirements
|
6
|
+
|
7
|
+
AppleBot requires [CasperJS](http://casperjs.org/), which can be installed using [Homebrew](http://brew.sh/):
|
8
|
+
|
9
|
+
```
|
10
|
+
$ brew install casperjs --devel
|
11
|
+
```
|
12
|
+
|
13
|
+
## Installation
|
14
|
+
|
15
|
+
```
|
16
|
+
$ gem install applebot
|
17
|
+
```
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
### Commands
|
22
|
+
|
23
|
+
These commands run using iTunes Connect:
|
24
|
+
|
25
|
+
- `app:create` - Creates an entry for a new app on iTunes Connect
|
26
|
+
- `app:update` - Creates an entry for a new update to an existing app on iTunes Connect
|
27
|
+
- `app:reject_binary` - Rejects the binary for a pending release (either new or an update)
|
28
|
+
- `app:remove_from_sale` - Removes the app from sale in all territories
|
29
|
+
|
30
|
+
These commands run using the Apple Developer Center:
|
31
|
+
|
32
|
+
- `app_id:create` - Creates a bundle identifier
|
33
|
+
- `app_id:delete` - Removes a bundle identifier
|
34
|
+
- `profile:create` - Creates a provisioning profile
|
35
|
+
- `profile:download` - Downloads an existing provisioning profile
|
36
|
+
- `profile:list` - Lists all provisioning profiles
|
37
|
+
|
38
|
+
These commands can run on either iTunes Connect or Apple Developer Center:
|
39
|
+
|
40
|
+
- `app_id:list` - Lists all bundle identifiers
|
41
|
+
|
42
|
+
#### JSON Manifests
|
43
|
+
|
44
|
+
Most commands take in options - you can either pass them individually, or use a JSON manifest file like this:
|
45
|
+
|
46
|
+
```json
|
47
|
+
{
|
48
|
+
"name": "My new app",
|
49
|
+
"app_id": "com.usepropeller.mynewapp"
|
50
|
+
}
|
51
|
+
```
|
52
|
+
|
53
|
+
For example, the following commands are equivalent usng this JSON manifest:
|
54
|
+
|
55
|
+
```shell
|
56
|
+
$ applebot app_id:create --name 'My new app' --app_id 'com.usepropeller.mynewapp'
|
57
|
+
$ applebot app_id:create --manifest ./manifest.json
|
58
|
+
```
|
59
|
+
|
60
|
+
```ruby
|
61
|
+
> AppleBot.app_id.create(name: 'My new app', app_id: 'com.usepropeller.mynewapp')
|
62
|
+
> AppleBot.app_id.create(manifest './manifest.json')
|
63
|
+
```
|
64
|
+
|
65
|
+
### CLI
|
66
|
+
|
67
|
+
AppleBot installs an `applebot` command, which you can explore with `-h` flags:
|
68
|
+
|
69
|
+
```shell
|
70
|
+
$ applebot -h
|
71
|
+
|
72
|
+
Commands:
|
73
|
+
app:create Create App
|
74
|
+
app:reject_binary Reject the binary of an pending release
|
75
|
+
app:remove_from_sale Remove app from sale
|
76
|
+
app:update Update App
|
77
|
+
app_id:create Create App ID
|
78
|
+
app_id:delete Delete App ID
|
79
|
+
app_id:list List App IDs
|
80
|
+
help Display global or [command] help documentation.
|
81
|
+
profile:create Create Provisioning Profile
|
82
|
+
profile:download Download a Provisioning Profile
|
83
|
+
profile:list List Provisioning Profiles
|
84
|
+
|
85
|
+
Global Options:
|
86
|
+
--manifest FILE.json Use a JSON file to load options for each command
|
87
|
+
--username USERNAME Username to login to Apple Service, or $APPLEBOT_USERNAME
|
88
|
+
--password PASSWORD Password to login to Apple Service, or $APPLEBOT_PASSWORD
|
89
|
+
--format FORMAT Output format - ['json', 'pretty']
|
90
|
+
--verbose Verbose output
|
91
|
+
```
|
92
|
+
|
93
|
+
#### Authentication
|
94
|
+
|
95
|
+
For every command, you can pass `--username` and `--password` flags to enter you auth credentials; you can also set `APPLEBOT_USERNAME` and `APPLEBOT_PASSWORD` environment variables.
|
96
|
+
|
97
|
+
### Ruby
|
98
|
+
|
99
|
+
The Ruby library uses an `AppleBot` module, and its methods map to the CLI commands:
|
100
|
+
|
101
|
+
```ruby
|
102
|
+
> require 'applebot'
|
103
|
+
=> true
|
104
|
+
> AppleBot.app.create(options: here)
|
105
|
+
```
|
106
|
+
|
107
|
+
#### Authentication
|
108
|
+
|
109
|
+
The Ruby library has a few shortcuts for logging in to Apple services:
|
110
|
+
|
111
|
+
```ruby
|
112
|
+
# pass as options
|
113
|
+
AppleBot.app.create(username: "username", password: "password")
|
114
|
+
|
115
|
+
# run in block
|
116
|
+
AppleBot.with_credentials(username: "username", password: "password") do
|
117
|
+
AppleBot.app.create(options)
|
118
|
+
end
|
119
|
+
|
120
|
+
# set globally
|
121
|
+
AppleBot.set_credentials(username: "username", password: "password")
|
122
|
+
```
|
123
|
+
|
124
|
+
|
125
|
+
### Output
|
126
|
+
|
127
|
+
The `:list` commands are meant to return some data. If you're using the Ruby library, you'll receive an `Array` when the command is done; if you're using the CLI, the last line will output a JSON object with one entry.
|
128
|
+
|
129
|
+
```ruby
|
130
|
+
AppleBot.app_ids.all
|
131
|
+
=> ["com.usepropeller.myapp"]
|
132
|
+
```
|
133
|
+
|
134
|
+
```bash
|
135
|
+
$ applebot app_ids:all
|
136
|
+
{"app_ids": ["com.usepropeller.myapp"]}
|
137
|
+
```
|
138
|
+
|
139
|
+
If you're using any other command (which generally create side-effects), the end result will be `true` in Ruby, or exit code 0 on the CLI.
|
140
|
+
|
141
|
+
#### Verbose & Pretty Output
|
142
|
+
|
143
|
+
You can base a `--verbose` flag (or a `verbose: true` option in Ruby) to see all of the output as each script processes. There are two output formats, `json` and `pretty`, which you are set with either the `--format` flag or `format: 'format_string'` in Ruby.
|
144
|
+
|
145
|
+
## Contact
|
146
|
+
|
147
|
+
[Clay Allsopp](http://clayallsopp.com/)
|
148
|
+
- [clay@usepropeller.com](mailto:clay@usepropeller.com)
|
149
|
+
- [@clayallsopp](https://twitter.com/clayallsopp)
|
150
|
+
|
151
|
+
## License
|
152
|
+
|
153
|
+
AppleBot is available under the MIT license. See the [LICENSE](LICENSE) file for more info.
|
data/Rakefile
ADDED
data/applebot.gemspec
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
|
4
|
+
require "applebot/version"
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = "applebot"
|
8
|
+
s.license = "MIT"
|
9
|
+
s.authors = ["Clay Allsopp"]
|
10
|
+
s.email = "clay@usepropeller.com"
|
11
|
+
s.homepage = AppleBot::WEBSITE
|
12
|
+
s.version = AppleBot::VERSION
|
13
|
+
s.summary = "AppleBot"
|
14
|
+
s.description = AppleBot::DESCRIPTION
|
15
|
+
|
16
|
+
s.add_dependency "commander", "~> 4.1.2"
|
17
|
+
s.add_dependency "activesupport", ">= 3.2"
|
18
|
+
s.add_dependency "terminal-table", ">= 1.4.0"
|
19
|
+
|
20
|
+
s.add_development_dependency "rake"
|
21
|
+
s.add_development_dependency "dotenv"
|
22
|
+
|
23
|
+
s.files = Dir["./**/*"].reject { |file| file =~ /\.\/(bin|log|pkg|script|spec|test|vendor)/ }
|
24
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
25
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
26
|
+
s.require_paths = ["lib"]
|
27
|
+
end
|
data/bin/applebot
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'commander/import'
|
4
|
+
|
5
|
+
applebot = File.join(File.dirname(__FILE__), '..', 'lib', 'applebot')
|
6
|
+
require applebot
|
7
|
+
|
8
|
+
HighLine.track_eof = false # Fix for built-in Ruby
|
9
|
+
Signal.trap("INT") {} # Suppress backtrace when exiting command
|
10
|
+
|
11
|
+
program :name, 'AppleBot'
|
12
|
+
program :version, AppleBot::VERSION
|
13
|
+
program :description, AppleBot::DESCRIPTION
|
14
|
+
|
15
|
+
program :help, 'Author', 'Clay Allsopp <clay@usepropeller.com>'
|
16
|
+
program :help, 'Website', AppleBot::WEBSITE
|
17
|
+
program :help_formatter, :compact
|
18
|
+
|
19
|
+
default_command :help
|
20
|
+
|
21
|
+
global_option('--manifest FILE.json', 'Use a JSON file to load options for each command') { |file|
|
22
|
+
$manifest_file = file
|
23
|
+
}
|
24
|
+
|
25
|
+
global_option('--username USERNAME', 'Username to login to Apple Service, or $APPLEBOT_USERNAME') { |username|
|
26
|
+
$username = username
|
27
|
+
}
|
28
|
+
|
29
|
+
global_option('--password PASSWORD', 'Password to login to Apple Service, or $APPLEBOT_PASSWORD') { |password|
|
30
|
+
$password = password
|
31
|
+
}
|
32
|
+
|
33
|
+
global_option('--format FORMAT', "Output format - 'json' or 'pretty'") { |format|
|
34
|
+
$format = format
|
35
|
+
}
|
36
|
+
|
37
|
+
global_option('--verbose', "Verbose output") {|verbose|
|
38
|
+
$verbose = verbose
|
39
|
+
}
|
40
|
+
|
41
|
+
AppleBot.commands.each do |apple_command|
|
42
|
+
command apple_command.cli_command.to_sym do |c|
|
43
|
+
c.syntax = "applebot #{apple_command.cli_command} [options]"
|
44
|
+
c.description = apple_command.description
|
45
|
+
c.summary = apple_command.description
|
46
|
+
|
47
|
+
(apple_command.options.required + apple_command.options.optional).each do |apple_option|
|
48
|
+
c.option "--#{apple_option.key} VALUE", apple_option.cli_description
|
49
|
+
end
|
50
|
+
|
51
|
+
c.action do |args, options|
|
52
|
+
user_options = {
|
53
|
+
manifest: $manifest_file,
|
54
|
+
format: $format || 'pretty',
|
55
|
+
verbose: $verbose,
|
56
|
+
print_result: true
|
57
|
+
}.merge(options.__hash__)
|
58
|
+
|
59
|
+
credentials = {username: $username, password: $password}
|
60
|
+
|
61
|
+
ret_value = false
|
62
|
+
AppleBot.with_credentials(credentials) do |ab|
|
63
|
+
ret_value = ab.run_command(apple_command.ruby_method, user_options)
|
64
|
+
end
|
65
|
+
$username = nil
|
66
|
+
$password = nil
|
67
|
+
$manifest_file = nil
|
68
|
+
$format = nil
|
69
|
+
$verbose = nil
|
70
|
+
ret_value
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
data/create_manifest.js
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
{
|
2
|
+
"username": "clay+applestore@usepropeller.com",
|
3
|
+
"password": "B0nnyB3ar",
|
4
|
+
"title": "Test App 9007",
|
5
|
+
"sku": "TEST_APP_9007",
|
6
|
+
"id": "com.usepropeller.test.test9007",
|
7
|
+
"price_tier": "free",
|
8
|
+
"app_version": "1.0.0",
|
9
|
+
"keywords": "this,thing,cool",
|
10
|
+
"support_url": "http://usepropeller.com",
|
11
|
+
"description": "My cool app",
|
12
|
+
"simulated_gambling": "none",
|
13
|
+
"horror_fear_themes": "none",
|
14
|
+
"primary_category": "book",
|
15
|
+
"secondary_category": "catalogs",
|
16
|
+
"first_name": "Clay",
|
17
|
+
"last_name": "Allsopp",
|
18
|
+
"email": "Clay.allsopp@gmail.com",
|
19
|
+
"phone": "9197937595",
|
20
|
+
"copyright": "Thing 2013",
|
21
|
+
"large_icon": "/Users/clayallsopp/Desktop/itunes_1.jpg",
|
22
|
+
"screenshots_35": "[\"/Users/clayallsopp/Projects/Apptory/iOS/profiles/apps/705/screenshots/products_35.png\"]",
|
23
|
+
"screenshots_4": "[\"/Users/clayallsopp/Projects/Apptory/iOS/profiles/apps/705/screenshots/products_4.png\"]"
|
24
|
+
}
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module AppleBot
|
2
|
+
class CommandProxy
|
3
|
+
class_attribute :proxies
|
4
|
+
self.proxies = {}
|
5
|
+
|
6
|
+
attr_accessor :namespace, :commands
|
7
|
+
|
8
|
+
def self.for(command)
|
9
|
+
proxies[command.namespace] ||= new(command.namespace)
|
10
|
+
proxy = proxies[command.namespace]
|
11
|
+
proxy.add_command(command)
|
12
|
+
proxy
|
13
|
+
end
|
14
|
+
|
15
|
+
def initialize(namespace)
|
16
|
+
@namespace = namespace
|
17
|
+
@commands = []
|
18
|
+
end
|
19
|
+
|
20
|
+
def add_command(command)
|
21
|
+
return if @commands.include?(command.action)
|
22
|
+
@commands << command.action
|
23
|
+
define_singleton_method(command.action, ->(options = {}) {
|
24
|
+
AppleBot.run_command(command.file_name, options)
|
25
|
+
})
|
26
|
+
end
|
27
|
+
|
28
|
+
def attach_to(klass)
|
29
|
+
return if klass.respond_to?(self.namespace)
|
30
|
+
namespace = self.namespace
|
31
|
+
klass.define_singleton_method(namespace) {
|
32
|
+
return AppleBot::CommandProxy.proxies[namespace]
|
33
|
+
}
|
34
|
+
end
|
35
|
+
|
36
|
+
def inspect
|
37
|
+
s = self.to_s.gsub(">", " ")
|
38
|
+
s << "commands: #{@commands.join(', ')}"
|
39
|
+
s << ">"
|
40
|
+
s
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
module AppleBot
|
2
|
+
mattr_accessor :commands
|
3
|
+
|
4
|
+
class Command < Struct.new(:file_name, :namespace, :action, :description, :options)
|
5
|
+
def initialize(*properties)
|
6
|
+
options = properties.delete_at -1
|
7
|
+
super(*properties)
|
8
|
+
self.options = CommandOptionSet.new(options['required'], options['optional'])
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.load_all!
|
12
|
+
commands_file_path = File.join(AppleBot.phantom_scripts_path, '_commands.json')
|
13
|
+
commands = JSON.parse(IO.read(commands_file_path))
|
14
|
+
AppleBot.commands = commands.keys.map {|file|
|
15
|
+
command_config = commands[file]
|
16
|
+
args = [file] + [
|
17
|
+
command_config['namespace'], command_config['action'],
|
18
|
+
command_config['description'], command_config['options']
|
19
|
+
]
|
20
|
+
Command.new(*args)
|
21
|
+
}
|
22
|
+
end
|
23
|
+
|
24
|
+
def cli_command
|
25
|
+
"#{namespace}:#{action}"
|
26
|
+
end
|
27
|
+
|
28
|
+
def ruby_method
|
29
|
+
"#{action}_#{namespace}"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
class CommandOptionSet < Struct.new(:required, :optional)
|
34
|
+
def initialize(required, optional)
|
35
|
+
fix_batch_options = lambda { |set, extras|
|
36
|
+
set.map {|o|
|
37
|
+
if o['batch']
|
38
|
+
o['keys'].map {|k|
|
39
|
+
h = {
|
40
|
+
'key' => k
|
41
|
+
}.merge(o)
|
42
|
+
h.delete 'keys'
|
43
|
+
h.delete 'batch'
|
44
|
+
h
|
45
|
+
}
|
46
|
+
else
|
47
|
+
o
|
48
|
+
end
|
49
|
+
}.flatten.map {|o|
|
50
|
+
o = o.merge(extras)
|
51
|
+
CommandOption.new(o['key'], o['description'], o['values'], o['default'], o['required'])
|
52
|
+
}
|
53
|
+
}
|
54
|
+
required = fix_batch_options.call(required, {'required' => true}) if required
|
55
|
+
required ||= []
|
56
|
+
optional = fix_batch_options.call(optional, {'required' => false}) if optional
|
57
|
+
optional ||= []
|
58
|
+
super(required, optional)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
class CommandOption < Struct.new(:key, :description, :values, :default, :required)
|
63
|
+
def cli_description
|
64
|
+
s = ""
|
65
|
+
s << "REQUIRED " if required
|
66
|
+
s << "#{description}"
|
67
|
+
s.tap do |d|
|
68
|
+
d << " - VALUES: #{values}" if values
|
69
|
+
d << " - DEFAULT: #{default}" if default
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
AppleBot::Command.load_all!
|
@@ -0,0 +1,97 @@
|
|
1
|
+
module AppleBot
|
2
|
+
class AppleBotError < StandardError
|
3
|
+
class_attribute :message
|
4
|
+
self.message = "Unspecified AppleBot error occured"
|
5
|
+
attr_reader :output, :html
|
6
|
+
|
7
|
+
def initialize(error, output = [], message = nil)
|
8
|
+
super(self.class.message)
|
9
|
+
output ||= []
|
10
|
+
output = output.map(&:strip).reject(&:blank?)
|
11
|
+
@html = output.select {|n|
|
12
|
+
is_debug_html?(n)
|
13
|
+
}.map { |s|
|
14
|
+
JSON.parse(s)['html']
|
15
|
+
}.first
|
16
|
+
|
17
|
+
@output = output.reject {|n|
|
18
|
+
is_debug_html?(n)
|
19
|
+
}
|
20
|
+
|
21
|
+
full_backtrace = (@output.map { |o|
|
22
|
+
"#{o}:in `AppleBot'"
|
23
|
+
} + error.backtrace).compact
|
24
|
+
set_backtrace(full_backtrace)
|
25
|
+
end
|
26
|
+
|
27
|
+
def is_debug_html?(line)
|
28
|
+
line.include?('"event":"debug_html"')
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
class AppIdNotFound < AppleBotError
|
33
|
+
self.message = "Could not find App Bundle ID in the HTML"
|
34
|
+
end
|
35
|
+
|
36
|
+
class AppleMaintenanceMode < AppleBotError
|
37
|
+
self.message = "Apple is currently in maintenance mode"
|
38
|
+
end
|
39
|
+
|
40
|
+
class AuthenticationError < AppleBotError
|
41
|
+
self.message = "Provided Apple ID credentials were incorrect"
|
42
|
+
end
|
43
|
+
|
44
|
+
class MultipleProfileError < AppleBotError
|
45
|
+
self.message = "Attempted to re-create an existing profile"
|
46
|
+
end
|
47
|
+
|
48
|
+
class AppNameTakenError < AppleBotError
|
49
|
+
self.message = "The app name you entered is taken"
|
50
|
+
end
|
51
|
+
|
52
|
+
class SignalTermination < StandardError
|
53
|
+
def initialize(status)
|
54
|
+
super("The process was terminated with signal #{status.termsig} (#{Signal.signame(status.termsig)})")
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
MESSAGE_FRAGMENT_TO_ERROR = {
|
59
|
+
"could not find App ID in options" => AppIdNotFound,
|
60
|
+
'class="maintenance"' => AppleMaintenanceMode,
|
61
|
+
'iTunes Connect is temporarily unavailable' => AppleMaintenanceMode,
|
62
|
+
'Your Apple ID or password was entered incorrectly' => AuthenticationError,
|
63
|
+
'Multiple profiles found with the name' => MultipleProfileError,
|
64
|
+
'The App Name you entered has already been used' => AppNameTakenError,
|
65
|
+
}
|
66
|
+
|
67
|
+
class AppleBotError
|
68
|
+
def self.for_output(output = [])
|
69
|
+
output ||= []
|
70
|
+
|
71
|
+
error_class = nil
|
72
|
+
|
73
|
+
output.each { |line|
|
74
|
+
MESSAGE_FRAGMENT_TO_ERROR.each { |message, klass|
|
75
|
+
if line.include?(message)
|
76
|
+
error_class = klass
|
77
|
+
break
|
78
|
+
end
|
79
|
+
}
|
80
|
+
break if error_class
|
81
|
+
}
|
82
|
+
error_class ||= AppleBotError
|
83
|
+
|
84
|
+
begin
|
85
|
+
# capture the Ruby-level stack trace
|
86
|
+
raise StandardError
|
87
|
+
rescue Exception => e
|
88
|
+
error_class.new(e, output)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def self.test
|
93
|
+
raise for_output(["something", "happened"])
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module AppleBot
|
2
|
+
module_function
|
3
|
+
|
4
|
+
# ripped from mkmf source
|
5
|
+
def find_executable(bin, path = nil)
|
6
|
+
ext = ""
|
7
|
+
if File.expand_path(bin) == bin
|
8
|
+
return bin if File.executable?(bin)
|
9
|
+
ext and File.executable?(file = bin + ext) and return file
|
10
|
+
return nil
|
11
|
+
end
|
12
|
+
if path ||= ENV['PATH']
|
13
|
+
path = path.split(File::PATH_SEPARATOR)
|
14
|
+
else
|
15
|
+
path = %w[/usr/local/bin /usr/ucb /usr/bin /bin]
|
16
|
+
end
|
17
|
+
file = nil
|
18
|
+
path.each do |dir|
|
19
|
+
return file if File.executable?(file = File.join(dir, bin))
|
20
|
+
return file if ext and File.executable?(file << ext)
|
21
|
+
end
|
22
|
+
nil
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
module AppleBot
|
2
|
+
class Shell
|
3
|
+
def command(sys_command, verbose, format)
|
4
|
+
output = []
|
5
|
+
Open3.popen3(sys_command) do |stdin, stdout, stderr, wait_thr|
|
6
|
+
while line = (stdout.gets || stderr.gets)
|
7
|
+
output << line
|
8
|
+
if verbose === true
|
9
|
+
puts_format_line(line, format)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
exit_status = wait_thr.value
|
13
|
+
output << JSON.generate(process_status: exit_status.inspect, thread: wait_thr.inspect)
|
14
|
+
if exit_status.termsig
|
15
|
+
raise SignalTermination.new(exit_status)
|
16
|
+
end
|
17
|
+
unless exit_status.success?
|
18
|
+
raise AppleBotError.for_output(output)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
output.select {|o|
|
23
|
+
o.include?('"result":')
|
24
|
+
}.last
|
25
|
+
end
|
26
|
+
|
27
|
+
def puts_format_line(line, format)
|
28
|
+
json = JSON.parse(line)
|
29
|
+
|
30
|
+
case format.to_s
|
31
|
+
when 'json'
|
32
|
+
puts JSON.generate(json.except('normal_output'))
|
33
|
+
else
|
34
|
+
normal_output = json['normal_output']
|
35
|
+
return if normal_output.blank?
|
36
|
+
return if normal_output.include?("[phantom]")
|
37
|
+
puts(normal_output)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def result(output, format, print_result)
|
42
|
+
result = JSON.parse(output)['result']
|
43
|
+
|
44
|
+
case format.to_s
|
45
|
+
when 'pretty'
|
46
|
+
table(result).tap do |t|
|
47
|
+
puts t if print_result
|
48
|
+
end
|
49
|
+
true
|
50
|
+
else
|
51
|
+
result.tap do |json|
|
52
|
+
puts json if print_result
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def table(data)
|
58
|
+
table = nil
|
59
|
+
if data.first && data.first[-1].is_a?(Hash)
|
60
|
+
headings = ['key'] + data.first[-1].keys
|
61
|
+
rows = data.to_a.map { |key_and_value|
|
62
|
+
row = []
|
63
|
+
headings.each do |heading|
|
64
|
+
if heading == 'key'
|
65
|
+
row << key_and_value[0]
|
66
|
+
else
|
67
|
+
row << key_and_value[1][heading]
|
68
|
+
end
|
69
|
+
end
|
70
|
+
row
|
71
|
+
}
|
72
|
+
table = Terminal::Table.new headings: headings, rows: rows
|
73
|
+
else
|
74
|
+
table = Terminal::Table.new rows: data.to_a
|
75
|
+
end
|
76
|
+
table
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|