deployto 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md ADDED
@@ -0,0 +1,27 @@
1
+ # deploytool
2
+
3
+ Deployment tool for Platform-as-a-Service providers, with special support for multi-stage deploys (e.g. for staging environments).
4
+
5
+ Platforms currently supported:
6
+ * Efficient Cloud Platforms
7
+
8
+ Platform support planned:
9
+ * Heroku
10
+ * Cloud Foundry Platforms
11
+
12
+ ## Deploying a Ruby on Rails app
13
+
14
+ gem install deploytool
15
+ deploy add production portfolio.heroku.com
16
+ deploy add staging portfolio.vcap.me
17
+ deploy production
18
+
19
+ ## Config file
20
+
21
+ deploy keeps a local config file .deployrc within the top-level sourcecode directory (determined by location of .git directory).
22
+
23
+ ## Legalese
24
+
25
+ _Copyright 2011, Efficient Cloud Ltd.
26
+
27
+ Released under the [MIT license](http://www.opensource.org/licenses/mit-license.php).
data/bin/deploy ADDED
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ lib = File.expand_path(File.dirname(__FILE__) + '/../lib')
4
+ $LOAD_PATH.unshift(lib) if File.directory?(lib) && !$LOAD_PATH.include?(lib)
5
+
6
+ require 'logger'
7
+ require 'rubygems'
8
+ require 'deploytool'
9
+ require 'deploytool/command'
10
+
11
+ $logger = Logger.new STDOUT
12
+ $logger.formatter = proc do |severity, datetime, progname, msg|
13
+ next if severity == "DEBUG"
14
+ if severity == "ERROR"
15
+ "ERROR: #{msg}\n"
16
+ else
17
+ "#{msg}\n"
18
+ end
19
+ end
20
+ HighLine.track_eof = false
21
+
22
+ args = ARGV.dup
23
+ ARGV.clear
24
+ command = args.shift.strip rescue 'help'
25
+
26
+ DeployTool::Command.run(command, args)
@@ -0,0 +1,108 @@
1
+ class DeployTool::Command
2
+ COMMANDS = ["to", "logs", "import", "export", "config"]
3
+
4
+ def self.run(command, args)
5
+ change_to_toplevel_dir!
6
+
7
+ DeployTool::Config.load(".deployrc")
8
+
9
+ if command == "help"
10
+ puts "Deploytool Usage Instructions"
11
+ puts ""
12
+ puts "Add a target:"
13
+ puts " deploy add production app1@demo.efficientcloud.com"
14
+ puts " deploy add staging myapp.heroku.com"
15
+ puts " deploy add failover api.cloudfoundry.com"
16
+ puts ""
17
+ puts "Deploy the current directory to the target:"
18
+ puts " deploy to production"
19
+ elsif command == "add"
20
+ if args[0].nil?
21
+ puts "ERROR: Missing target name."
22
+ puts ""
23
+ puts "Use \"deploy help\" if you're lost."
24
+ exit
25
+ end
26
+ if args[1].nil?
27
+ puts "ERROR: Missing target specification."
28
+ puts ""
29
+ puts "Use \"deploy help\" if you're lost."
30
+ exit
31
+ end
32
+ unless target = DeployTool::Target.find(args[1])
33
+ puts "ERROR: Couldn't find provider for target \"#{args[1]}\""
34
+ puts ""
35
+ puts "Use \"deploy help\" if you're lost."
36
+ exit
37
+ end
38
+ DeployTool::Config[args[0]] = target.to_h
39
+ elsif command == "list"
40
+ puts "Registered Targets:"
41
+ DeployTool::Config.all.each do |target_name, target|
42
+ target = DeployTool::Target.from_config(target)
43
+ puts " %s%s" % [target_name.ljust(15), target.to_s]
44
+ end
45
+ else
46
+ args.unshift command unless command == "to"
47
+ target_name = args[0]
48
+
49
+ unless (target = DeployTool::Config[target_name]) && !target.nil? && target.size > 0
50
+ puts "ERROR: Couldn't find target: #{target_name}"
51
+ puts ""
52
+ puts "Use \"deploy help\" if you're lost."
53
+ exit
54
+ end
55
+
56
+ opts = {}
57
+ opts[:timing] = true if args.include?("--timing")
58
+
59
+ target = DeployTool::Target.from_config(target)
60
+ begin
61
+ target.push(opts)
62
+ rescue => e
63
+ puts e
64
+ exit 2
65
+ end
66
+ end
67
+
68
+ DeployTool::Config.save
69
+ rescue Net::HTTPServerException => e
70
+ $logger.info "ERROR: HTTP call returned %s %s" % [e.response.code, e.response.message]
71
+ if target
72
+ $logger.debug "\nTarget:"
73
+ target.to_h.each do |k, v|
74
+ next if k.to_sym == :password
75
+ $logger.debug " %s = %s" % [k, v]
76
+ end
77
+ end
78
+ $logger.debug "\nBacktrace:"
79
+ $logger.debug " " + e.backtrace.join("\n ")
80
+ $logger.debug "\nResponse:"
81
+ e.response.each_header do |k, v|
82
+ $logger.debug " %s: %s" % [k, v]
83
+ end
84
+ $logger.debug "\n " + e.response.body.gsub("\n", "\n ")
85
+ $logger.info "\nPlease run again with \"--debug\" and report the output at http://bit.ly/deploytool-new-issue"
86
+ exit 2
87
+ end
88
+
89
+ # Tries to figure out if we're running in a subdirectory of the source,
90
+ # and switches to the top-level if that's the case
91
+ def self.change_to_toplevel_dir!
92
+ indicators = [".git", "Gemfile", "LICENSE", "test"]
93
+
94
+ timeout = 10
95
+ path = Dir.pwd
96
+ begin
97
+ indicators.each do |indicator|
98
+ next unless File.exists?(File.join(path, indicator))
99
+
100
+ $logger.debug "Found correct top-level directory %s, switching working directory." % [path] unless path == Dir.pwd
101
+ Dir.chdir path
102
+ return
103
+ end
104
+ end until (path = File.dirname(path)) == "/" || (timeout -= 1) == 0
105
+
106
+ $logger.debug "DEBUG: Couldn't locate top-level directory (traversed until %s), falling back to %s" % [path, Dir.pwd]
107
+ end
108
+ end
@@ -0,0 +1,21 @@
1
+ require 'inifile'
2
+
3
+ class DeployTool::Config
4
+ def self.all
5
+ @@configfile.to_h
6
+ end
7
+ def self.[](section)
8
+ @@configfile[section]
9
+ end
10
+ def self.[]=(section, value)
11
+ @@configfile[section] = value
12
+ end
13
+
14
+ def self.load(filename)
15
+ @@configfile = IniFile.load(filename)
16
+ end
17
+
18
+ def self.save
19
+ @@configfile.save unless @@configfile.to_h.empty?
20
+ end
21
+ end
@@ -0,0 +1,31 @@
1
+ class DeployTool::Target::CloudFoundry < DeployTool::Target
2
+ def self.parse_target_spec(target_spec)
3
+ app_name, server = target_spec.split('.', 2)
4
+ return false if server.nil? or app_name.nil?
5
+ # Test through multiple versions of API server URLs
6
+ [target_spec, 'api.' + target_spec, 'api.' + server].each do |api_server|
7
+ begin
8
+ if get_json_resource("http://%s/info" % api_server)['name'] == "vcap"
9
+ app_name = nil if server.gsub('api.') == api_server.gsub('api.')
10
+ return [app_name, api_server]
11
+ end
12
+ rescue => e
13
+ $logger.debug "Exception: %s\n%s" % [e.message, e.backtrace.join("\n")]
14
+ end
15
+ end
16
+ false
17
+ end
18
+
19
+ def self.matches?(target_spec)
20
+ return true if parse_target_spec(target_spec)
21
+ end
22
+
23
+ def initialize(options)
24
+ # FIXME
25
+ end
26
+
27
+ def self.create(target_spec)
28
+ app_name, api_server = parse_target_spec(target_spec)
29
+ CloudFoundry.new('api_server' => api_server, 'app_name' => app_name)
30
+ end
31
+ end
@@ -0,0 +1,146 @@
1
+ require 'rexml/document'
2
+ require 'addressable/uri'
3
+ require 'net/http'
4
+ require 'net/http/post/multipart'
5
+ require 'fileutils'
6
+ require 'tempfile'
7
+ require 'zip'
8
+
9
+ class DeployTool::Target::EfficientCloud
10
+ class ApiClient
11
+ attr_reader :server, :app_id, :email, :password
12
+ def initialize(server, app_id, email, password)
13
+ @app_id = app_id
14
+ @server = server
15
+ @email = email
16
+ @password = password
17
+ end
18
+
19
+ def call(method, method_name, data = {})
20
+ url = Addressable::URI.parse("http://#{@server}/api/cli/v1/apps/#{@app_id}/#{method_name}")
21
+ data = data.merge(:email => @email, :password => @password)
22
+ if method == :post
23
+ res = Net::HTTP.start(url.host, url.port) do |http|
24
+ http.request Net::HTTP::Post::Multipart.new(url.path, data)
25
+ end
26
+ else
27
+ url.query_values = data
28
+ res = Net::HTTP.get_response(url)
29
+ end
30
+ case res
31
+ when Net::HTTPSuccess, Net::HTTPRedirection
32
+ res.body
33
+ else
34
+ res.error!
35
+ end
36
+ end
37
+
38
+ def upload
39
+ appfiles = Dir.glob('**/*', File::FNM_DOTMATCH).reject {|f| File.directory?(f) || f[/(^|\/).{1,2}$/] || f[/^.git\//] || f[/^.deployrc$/] || f[/(^|\/).DS_Store$/] }
40
+
41
+ # Construct a temporary zipfile
42
+ tempfile = Tempfile.open("ecli-upload.zip")
43
+ Zip::ZipOutputStream.open(tempfile.path) do |z|
44
+ appfiles.each do |appfile|
45
+ z.put_next_entry appfile
46
+ z.print IO.read(appfile)
47
+ end
48
+ end
49
+
50
+ puts "-----> Uploading %s code tarball..." % human_filesize(tempfile.path)
51
+ initial_response = call :post, 'upload', {:code => UploadIO.new(tempfile, "application/zip", "ecli-upload.zip")}
52
+ doc = REXML::Document.new initial_response
53
+ doc.elements["code/code-token"].text
54
+ rescue Net::HTTPServerException => e
55
+ case e.response
56
+ when Net::HTTPNotFound
57
+ $logger.error "Application app%d.%s couldn't be found." % [@app_id, @server.gsub('api.', '')]
58
+ when Net::HTTPUnauthorized
59
+ $logger.error "You're not authorized to update app%d.%s." % [@app_id, @server.gsub('api.', '')]
60
+ else
61
+ raise e
62
+ end
63
+ $logger.info "\nPlease check the controlpanel for update instructions."
64
+ exit 2
65
+ end
66
+
67
+ def deploy(code_token)
68
+ initial_response = call :post, 'deploy', {:code_token => code_token}
69
+ doc = REXML::Document.new initial_response
70
+ deploy_token = doc.elements["deploy/token"].text
71
+ deploy_token
72
+ end
73
+
74
+ def save_timing_data(data)
75
+ File.open('deploytool-timingdata-%d.json' % (Time.now), 'w') do |f|
76
+ f.puts data.to_json
77
+ end
78
+ end
79
+
80
+ def deploy_status(deploy_token, opts)
81
+ start = Time.now
82
+ timing = []
83
+ previous_status = nil
84
+ print "-----> Started deployment '%s'" % deploy_token
85
+
86
+ while true
87
+ sleep 1
88
+ resp = call :get, 'deploy_status', {:deploy_token => deploy_token}
89
+ doc = REXML::Document.new resp
90
+
91
+ if doc.elements["deploy/message"].nil?
92
+ puts resp
93
+ puts "...possibly done."
94
+ break
95
+ end
96
+ if doc.elements["deploy/message"].text == 'nojob'
97
+ puts "\n-----> FINISHED after %d seconds!" % (Time.now-start)
98
+ break
99
+ end
100
+
101
+ status = doc.elements["deploy/message"].text.gsub('["', '').gsub('"]', '')
102
+ if previous_status != status
103
+ case status
104
+ when "build"
105
+ puts "\n-----> Building/updating virtual machine..."
106
+ when "deploy"
107
+ print "-----> Copying virtual machine to app hosts"
108
+ when "publishing"
109
+ print "\n-----> Updating HTTP gateways"
110
+ when "cleanup"
111
+ print "\n-----> Removing old deployments"
112
+ end
113
+ previous_status = status
114
+ end
115
+
116
+ logs = doc.elements["deploy/logs"].text rescue nil
117
+ if logs
118
+ puts "" if status != "build" # Add newline after the dots
119
+ puts logs
120
+ timing << [Time.now-start, status, logs]
121
+ else
122
+ timing << [Time.now-start, status]
123
+ if status == 'error'
124
+ if logs.nil? or logs.empty?
125
+ raise "ERROR after %d seconds!" % (Time.now-start)
126
+ end
127
+ elsif status != "build"
128
+ print "."
129
+ STDOUT.flush
130
+ end
131
+ end
132
+ end
133
+ ensure
134
+ save_timing_data timing if opts[:timing]
135
+ end
136
+
137
+ def human_filesize(path)
138
+ size = File.size(path)
139
+ units = %w{B KB MB GB TB}
140
+ e = (Math.log(size)/Math.log(1024)).floor
141
+ s = "%.1f" % (size.to_f / 1024**e)
142
+ s.sub(/\.?0*$/, units[e])
143
+ end
144
+ end
145
+ end
146
+
@@ -0,0 +1,68 @@
1
+ require 'highline'
2
+
3
+ class DeployTool::Target::EfficientCloud < DeployTool::Target
4
+ SUPPORTED_API_VERSION = 1
5
+ def self.parse_target_spec(target_spec)
6
+ server, app_id = target_spec.split('@').reverse
7
+ if app_id.nil?
8
+ app_id = server.split('.', 2).first
9
+ end
10
+ [server, 'api.' + server, 'api.' + server.split('.', 2).last].each do |api_server|
11
+ begin
12
+ return [app_id.gsub('app', '').to_i, api_server] if check_version(api_server)
13
+ rescue => e
14
+ puts e
15
+ end
16
+ end
17
+ nil
18
+ end
19
+
20
+ def self.matches?(target_spec)
21
+ return true if parse_target_spec(target_spec)
22
+ end
23
+
24
+ def to_h
25
+ {:type => "EfficientCloud", :api_server => @api_client.server, :app_id => @api_client.app_id, :email => @api_client.email, :password => @api_client.password}
26
+ end
27
+
28
+ def to_s
29
+ "app%s@%s (EFC-based platform)" % [@api_client.app_id, @api_client.server]
30
+ end
31
+
32
+ def initialize(options)
33
+ @api_server = options['api_server']
34
+ @api_client = ApiClient.new(options['api_server'], options['app_id'], options['email'], options['password'])
35
+ end
36
+
37
+ def self.check_version(api_server)
38
+ begin
39
+ info = get_json_resource("http://%s/info" % api_server)
40
+ rescue => e
41
+ $logger.debug "Exception: %s\n%s" % [e.message, e.backtrace.join("\n")]
42
+ return false
43
+ end
44
+ return false unless info && info['name'] == "efc"
45
+
46
+ if info['api_version'] > SUPPORTED_API_VERSION
47
+ $logger.error "This version of deploytool is outdated.\nThis server requires at least API Version #{info['api_version']}."
48
+ end
49
+ return true
50
+ end
51
+
52
+ def self.create(target_spec)
53
+ $logger.info "Please specify your controlpanel login information"
54
+ email = HighLine.new.ask("E-mail: ")
55
+ password = HighLine.new.ask("Password: ") {|q| q.echo = "*" }
56
+ app_id, api_server = parse_target_spec(target_spec)
57
+ EfficientCloud.new('api_server' => api_server, 'app_id' => app_id, 'email' => email, 'password' => password)
58
+ end
59
+
60
+ def push(opts)
61
+ self.class.check_version(@api_server)
62
+ code_token = @api_client.upload
63
+ deploy_token = @api_client.deploy(code_token)
64
+ @api_client.deploy_status(deploy_token, opts) # Blocks till deploy is done
65
+ end
66
+ end
67
+
68
+ require 'deploytool/target/efficientcloud/api_client'
@@ -0,0 +1,30 @@
1
+ class DeployTool::Target::Heroku < DeployTool::Target
2
+ def self.matches?(target_spec)
3
+ target_spec[/(^|\.)heroku\.com$/]
4
+ end
5
+
6
+ def to_h
7
+ {:type => "Heroku", :app_name => @app_name}
8
+ end
9
+
10
+ def to_s
11
+ "%s.heroku.com (Heroku)" % [@app_name]
12
+ end
13
+
14
+ def initialize(options)
15
+ @app_name = options['app_name']
16
+ end
17
+
18
+ def self.create(target_name)
19
+ app_name = target_name.gsub('.heroku.com', '')
20
+ # TODO: Require current directory to be a git repository
21
+ # TODO: Ask for app name if app name is nil or www
22
+ puts `heroku create #{app_name}`
23
+ Heroku.new('app_name' => app_name)
24
+ end
25
+
26
+ def push(opts)
27
+ puts `git push -f git@heroku.com:#{@app_name}.git master`
28
+ $?.exitstatus
29
+ end
30
+ end
@@ -0,0 +1,64 @@
1
+ require 'json'
2
+
3
+ class Module
4
+ def track_subclasses
5
+ instance_eval %{
6
+ def self.known_subclasses
7
+ @__deploytool_subclasses
8
+ end
9
+
10
+ def self.add_known_subclass(s)
11
+ superclass.add_known_subclass(s) if superclass.respond_to?(:inherited_tracking_subclasses)
12
+ (@__deploytool_subclasses ||= []) << s
13
+ end
14
+
15
+ def self.inherited_tracking_subclasses(s)
16
+ add_known_subclass(s)
17
+ inherited_not_tracking_subclasses(s)
18
+ end
19
+ alias :inherited_not_tracking_subclasses :inherited
20
+ alias :inherited :inherited_tracking_subclasses
21
+ }
22
+ end
23
+ end
24
+
25
+ class DeployTool::Target
26
+ track_subclasses
27
+
28
+ def self.find(target_spec)
29
+ known_subclasses.each do |klass|
30
+ next unless klass.matches?(target_spec)
31
+ return klass.create(target_spec)
32
+ end
33
+ nil
34
+ end
35
+
36
+ def self.from_config(config)
37
+ known_subclasses.each do |klass|
38
+ next unless klass.to_s.split('::').last == config['type']
39
+ return klass.new(config)
40
+ end
41
+ nil
42
+ end
43
+
44
+ def self.get_json_resource(url)
45
+ res = nil
46
+ begin
47
+ timeout(5) do
48
+ res = Net::HTTP.get_response(Addressable::URI.parse(url))
49
+ end
50
+ rescue Timeout::Error
51
+ $logger.debug "Calling '%s' took longer than 5s, skipping" % [url, res.code, res.body]
52
+ return nil
53
+ end
54
+ if res.code != '200'
55
+ $logger.debug "Calling '%s' returned %s, skipping" % [url, res.code, res.body]
56
+ return nil
57
+ end
58
+ JSON.parse(res.body)
59
+ end
60
+ end
61
+
62
+ (Dir.glob(File.dirname(__FILE__)+'/target/*.rb') - [__FILE__]).sort.each do |f|
63
+ require 'deploytool/target/' + File.basename(f)
64
+ end
@@ -0,0 +1,3 @@
1
+ module DeployTool
2
+ VERSION = "0.9.0"
3
+ end
data/lib/deploytool.rb ADDED
@@ -0,0 +1,4 @@
1
+ module DeployTool; end
2
+
3
+ require 'deploytool/config'
4
+ require 'deploytool/target'
data/spec/spec.opts ADDED
@@ -0,0 +1 @@
1
+ --color
@@ -0,0 +1,16 @@
1
+ ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
2
+ require "rubygems"
3
+ require "bundler/setup"
4
+
5
+ require 'rspec'
6
+ require 'deploytool'
7
+ require 'logger'
8
+
9
+ RSpec.configure do |config|
10
+ config.mock_with :rr
11
+ end
12
+
13
+ $logger = Logger.new File.expand_path('../spec.log', __FILE__)
14
+ $logger.formatter = proc { |severity, datetime, progname, msg|
15
+ "#{severity} #{datetime.strftime("%Y-%m-%d %H:%M:%S")}: #{msg}\n"
16
+ }
@@ -0,0 +1,43 @@
1
+ require 'spec_helper'
2
+
3
+ describe DeployTool::Target do
4
+ context "Target selection" do
5
+ before do
6
+ stub.any_instance_of(HighLine).ask do |q, |
7
+ if q[/E-mail/]
8
+ "demo@efficientcloud.com"
9
+ elsif q[/Password/]
10
+ "demo"
11
+ else
12
+ nil
13
+ end
14
+ end
15
+
16
+ # TODO: Mock HTTP get method
17
+ end
18
+
19
+ ["api.cloudfoundry.com", "cloudfoundry.com", "awesomeapp.cloudfoundry.com"].each do |target_spec| #, "api.cloud.1and1.com", "cloud.1and1.com", "awesomeapp.cloud.1and1.com"].each do
20
+ it "should detect #{target_spec} as a CloudFoundry target" do
21
+ DeployTool::Target.find(target_spec).class.should == DeployTool::Target::CloudFoundry
22
+ end
23
+ end
24
+
25
+ ["app10000@api.srv.io", "app10000@srv.io", "app10000@app123.srv.io", "app10000.srv.io"].each do |target_spec|
26
+ it "should detect #{target_spec} as an Efficient Cloud target" do
27
+ DeployTool::Target.find(target_spec).class.should == DeployTool::Target::EfficientCloud
28
+ end
29
+ end
30
+
31
+ ["heroku.com", "awesomeapp.heroku.com"].each do |target_spec|
32
+ it "should detect #{target_spec} as an Heroku target" do
33
+ DeployTool::Target.find(target_spec).class.should == DeployTool::Target::Heroku
34
+ end
35
+ end
36
+
37
+ ["gandi.net", "1and1.com"].each do |target_spec|
38
+ it "should return an error with #{target_spec} as target" do
39
+ DeployTool::Target.find(target_spec).class.should == NilClass
40
+ end
41
+ end
42
+ end
43
+ end
metadata ADDED
@@ -0,0 +1,177 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: deployto
3
+ version: !ruby/object:Gem::Version
4
+ hash: 59
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 9
9
+ - 0
10
+ version: 0.9.0
11
+ platform: ruby
12
+ authors:
13
+ - Efficient Cloud Ltd
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2011-07-11 00:00:00 Z
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: inifile
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ none: false
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ hash: 13
29
+ segments:
30
+ - 0
31
+ - 4
32
+ - 1
33
+ version: 0.4.1
34
+ type: :runtime
35
+ version_requirements: *id001
36
+ - !ruby/object:Gem::Dependency
37
+ name: addressable
38
+ prerelease: false
39
+ requirement: &id002 !ruby/object:Gem::Requirement
40
+ none: false
41
+ requirements:
42
+ - - ">="
43
+ - !ruby/object:Gem::Version
44
+ hash: 3
45
+ segments:
46
+ - 0
47
+ version: "0"
48
+ type: :runtime
49
+ version_requirements: *id002
50
+ - !ruby/object:Gem::Dependency
51
+ name: multipart-post
52
+ prerelease: false
53
+ requirement: &id003 !ruby/object:Gem::Requirement
54
+ none: false
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ hash: 3
59
+ segments:
60
+ - 0
61
+ version: "0"
62
+ type: :runtime
63
+ version_requirements: *id003
64
+ - !ruby/object:Gem::Dependency
65
+ name: highline
66
+ prerelease: false
67
+ requirement: &id004 !ruby/object:Gem::Requirement
68
+ none: false
69
+ requirements:
70
+ - - ">="
71
+ - !ruby/object:Gem::Version
72
+ hash: 3
73
+ segments:
74
+ - 0
75
+ version: "0"
76
+ type: :runtime
77
+ version_requirements: *id004
78
+ - !ruby/object:Gem::Dependency
79
+ name: zip
80
+ prerelease: false
81
+ requirement: &id005 !ruby/object:Gem::Requirement
82
+ none: false
83
+ requirements:
84
+ - - ">="
85
+ - !ruby/object:Gem::Version
86
+ hash: 3
87
+ segments:
88
+ - 0
89
+ version: "0"
90
+ type: :runtime
91
+ version_requirements: *id005
92
+ - !ruby/object:Gem::Dependency
93
+ name: heroku
94
+ prerelease: false
95
+ requirement: &id006 !ruby/object:Gem::Requirement
96
+ none: false
97
+ requirements:
98
+ - - ">="
99
+ - !ruby/object:Gem::Version
100
+ hash: 3
101
+ segments:
102
+ - 0
103
+ version: "0"
104
+ type: :runtime
105
+ version_requirements: *id006
106
+ - !ruby/object:Gem::Dependency
107
+ name: json
108
+ prerelease: false
109
+ requirement: &id007 !ruby/object:Gem::Requirement
110
+ none: false
111
+ requirements:
112
+ - - ">="
113
+ - !ruby/object:Gem::Version
114
+ hash: 3
115
+ segments:
116
+ - 0
117
+ version: "0"
118
+ type: :runtime
119
+ version_requirements: *id007
120
+ description: Deployment tool with support for multiple Platform-as-a-Service providers.
121
+ email: hello@platformdirectory.com
122
+ executables:
123
+ - deploy
124
+ extensions: []
125
+
126
+ extra_rdoc_files: []
127
+
128
+ files:
129
+ - README.md
130
+ - bin/deploy
131
+ - lib/deploytool.rb
132
+ - lib/deploytool/command.rb
133
+ - lib/deploytool/config.rb
134
+ - lib/deploytool/target.rb
135
+ - lib/deploytool/target/cloudfoundry.rb
136
+ - lib/deploytool/target/efficientcloud.rb
137
+ - lib/deploytool/target/efficientcloud/api_client.rb
138
+ - lib/deploytool/target/heroku.rb
139
+ - lib/deploytool/version.rb
140
+ - spec/spec.opts
141
+ - spec/spec_helper.rb
142
+ - spec/target_spec.rb
143
+ homepage: http://platformdirectory.com/
144
+ licenses: []
145
+
146
+ post_install_message:
147
+ rdoc_options: []
148
+
149
+ require_paths:
150
+ - lib
151
+ required_ruby_version: !ruby/object:Gem::Requirement
152
+ none: false
153
+ requirements:
154
+ - - ">="
155
+ - !ruby/object:Gem::Version
156
+ hash: 3
157
+ segments:
158
+ - 0
159
+ version: "0"
160
+ required_rubygems_version: !ruby/object:Gem::Requirement
161
+ none: false
162
+ requirements:
163
+ - - ">="
164
+ - !ruby/object:Gem::Version
165
+ hash: 3
166
+ segments:
167
+ - 0
168
+ version: "0"
169
+ requirements: []
170
+
171
+ rubyforge_project:
172
+ rubygems_version: 1.8.5
173
+ signing_key:
174
+ specification_version: 3
175
+ summary: Multi-platform deployment tool.
176
+ test_files: []
177
+