shuttle-deploy 0.2.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +15 -0
  2. data/.gitignore +25 -0
  3. data/.magnum.yml +1 -0
  4. data/.rspec +2 -0
  5. data/Gemfile +3 -0
  6. data/LICENSE +18 -0
  7. data/README.md +230 -0
  8. data/Rakefile +10 -0
  9. data/bin/shuttle +60 -0
  10. data/examples/rails.yml +22 -0
  11. data/examples/static.yml +10 -0
  12. data/examples/wordpress.yml +34 -0
  13. data/lib/shuttle/config.rb +5 -0
  14. data/lib/shuttle/deploy.rb +58 -0
  15. data/lib/shuttle/deployment/nodejs.rb +48 -0
  16. data/lib/shuttle/deployment/php.rb +17 -0
  17. data/lib/shuttle/deployment/rails.rb +116 -0
  18. data/lib/shuttle/deployment/ruby.rb +40 -0
  19. data/lib/shuttle/deployment/static.rb +5 -0
  20. data/lib/shuttle/deployment/wordpress/cli.rb +27 -0
  21. data/lib/shuttle/deployment/wordpress/core.rb +50 -0
  22. data/lib/shuttle/deployment/wordpress/plugins.rb +112 -0
  23. data/lib/shuttle/deployment/wordpress/vip.rb +84 -0
  24. data/lib/shuttle/deployment/wordpress.rb +191 -0
  25. data/lib/shuttle/errors.rb +5 -0
  26. data/lib/shuttle/helpers.rb +50 -0
  27. data/lib/shuttle/runner.rb +153 -0
  28. data/lib/shuttle/session.rb +52 -0
  29. data/lib/shuttle/support/bundler.rb +45 -0
  30. data/lib/shuttle/support/foreman.rb +7 -0
  31. data/lib/shuttle/support/thin.rb +59 -0
  32. data/lib/shuttle/target.rb +23 -0
  33. data/lib/shuttle/tasks.rb +264 -0
  34. data/lib/shuttle/version.rb +3 -0
  35. data/lib/shuttle.rb +35 -0
  36. data/shuttle-deploy.gemspec +28 -0
  37. data/spec/deploy_spec.rb +4 -0
  38. data/spec/fixtures/.gitkeep +0 -0
  39. data/spec/fixtures/static.yml +11 -0
  40. data/spec/helpers_spec.rb +42 -0
  41. data/spec/spec_helper.rb +15 -0
  42. data/spec/target_spec.rb +41 -0
  43. metadata +232 -0
@@ -0,0 +1,153 @@
1
+ module Shuttle
2
+ class Runner
3
+ attr_reader :options
4
+ attr_reader :config_path
5
+ attr_reader :config, :target
6
+
7
+ def initialize(options)
8
+ @options = options
9
+ @config_path = File.expand_path(options[:path])
10
+ @target = options[:target]
11
+
12
+ if !File.exists?(config_path)
13
+ raise ConfigError, "Config file #{config_path} does not exist"
14
+ end
15
+
16
+ @config_path = config_path
17
+ @target = target
18
+ end
19
+
20
+ def load_config
21
+ data = File.read(config_path)
22
+
23
+ if config_path =~ /\.toml$/
24
+ parse_toml_data(data)
25
+ else
26
+ parse_yaml_data(data)
27
+ end
28
+ end
29
+
30
+ def parse_yaml_data(data)
31
+ Hashr.new(YAML.safe_load(data))
32
+ end
33
+
34
+ def parse_toml_data(data)
35
+ Hashr.new(TOML::Parser.new(data).parsed)
36
+ end
37
+
38
+ def validate_target(target)
39
+ if target.host.nil?
40
+ raise ConfigError, "Target host required"
41
+ end
42
+
43
+ if target.user.nil?
44
+ raise ConfigError, "Target user required"
45
+ end
46
+
47
+ if target.deploy_to.nil?
48
+ raise ConfigError, "Target deploy path required"
49
+ end
50
+ end
51
+
52
+ def execute(command)
53
+ @config = load_config
54
+
55
+ strategy = config.app.strategy || 'static'
56
+ if strategy.nil?
57
+ raise ConfigError, "Invalid strategy: #{strategy}"
58
+ end
59
+
60
+ if @config.target
61
+ server = @config.target
62
+ else
63
+ if @config.targets.nil?
64
+ raise ConfigError, "Please define deployment target"
65
+ end
66
+
67
+ server = @config.targets[target]
68
+ if server.nil?
69
+ raise ConfigError, "Target #{target} does not exist"
70
+ end
71
+ end
72
+
73
+ validate_target(server)
74
+
75
+ ssh = Net::SSH::Session.new(server.host, server.user, server.password)
76
+
77
+ if options[:log]
78
+ ssh.logger = Logger.new(STDOUT)
79
+ end
80
+
81
+ ssh.open
82
+
83
+ klass = Shuttle.const_get(strategy.capitalize) rescue nil
84
+
85
+ if klass.nil?
86
+ STDERR.puts "Invalid strategy: #{strategy}"
87
+ exit 1
88
+ end
89
+
90
+ integration = klass.new(config, ssh, server, target)
91
+
92
+ command.gsub!(/:/,'_')
93
+ exit_code = 0
94
+ puts "\n"
95
+
96
+ puts "Shuttle v#{Shuttle::VERSION}\n"
97
+ puts "\n"
98
+ integration.log "Connected to #{server.user}@#{server.host}"
99
+
100
+ if integration.respond_to?(command)
101
+ time_start = Time.now
102
+
103
+ begin
104
+ if integration.deploy_running?
105
+ deployer = ssh.read_file("#{integration.deploy_path}/.lock").strip
106
+ message = "Another deployment is running."
107
+ message << " Deployer: #{deployer}" if deployer.size > 0
108
+
109
+ integration.error(message)
110
+ end
111
+
112
+ integration.write_lock
113
+ integration.export_environment
114
+ integration.send(command.to_sym)
115
+ integration.write_revision
116
+
117
+ rescue DeployError => err
118
+ integration.cleanup_release
119
+ exit_code = 1
120
+ rescue SystemExit
121
+ # NOOP
122
+ exit_code = 0
123
+ rescue Interrupt
124
+ STDERR.puts "Interrupted by user. Aborting deploy..."
125
+ exit_code = 1
126
+ rescue Exception => err
127
+ integration.cleanup_release
128
+ integration.log("ERROR: #{err.message}", 'error')
129
+ exit_code = 1
130
+ ensure
131
+ integration.release_lock
132
+ end
133
+
134
+ if exit_code == 0
135
+ diff = (Float(Time.now - time_start) * 100).round / 100
136
+ duration = ChronicDuration.output(diff, :format => :short)
137
+ puts "\nExecution time: #{duration}\n"
138
+ end
139
+
140
+ puts "\n"
141
+ exit(exit_code)
142
+
143
+ else
144
+ raise ConfigError, "Invalid command: #{command}"
145
+ end
146
+
147
+ ssh.close
148
+ rescue Net::SSH::AuthenticationFailed
149
+ STDERR.puts "Authentication failed"
150
+ exit 1
151
+ end
152
+ end
153
+ end
@@ -0,0 +1,52 @@
1
+ module Shuttle
2
+ class Session
3
+ attr_reader :config, :target
4
+
5
+ # Initialize a new session
6
+ # @param [Hashr] deploy config
7
+ # @param [String] deploy target
8
+ def initialize(config, target)
9
+ @config = config
10
+ @target = target
11
+ end
12
+
13
+ def validate
14
+ if config.app.strategy.nil?
15
+ raise ConfigError, "Deployment strategy is required"
16
+ end
17
+
18
+ if config.targets[target].nil?
19
+ raise ConfigError, "Target does not exist"
20
+ end
21
+ end
22
+
23
+ def run(command)
24
+ strategy = config.app.strategy
25
+ server = config.targets[target]
26
+
27
+ ssh = Net::SSH::Session.new(server.host, server.user, server.password)
28
+ ssh.open
29
+
30
+ klass = Shuttle.const_get(strategy.capitalize)
31
+ integration = klass.new(config, ssh, server, target)
32
+
33
+ if integration.deploy_running?
34
+ raise DeployError, "Another deployment is running"
35
+ end
36
+
37
+ begin
38
+ integration.write_lock
39
+ integration.send(command.to_sym)
40
+ integration.write_revision
41
+ rescue DeployError => err
42
+ integration.cleanup_release
43
+ rescue Exception => err
44
+ integration.cleanup_release
45
+ ensure
46
+ integration.release_lock
47
+ end
48
+
49
+ ssh.close
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,45 @@
1
+ module Shuttle
2
+ module Support
3
+ module Bundler
4
+ def bundle_path
5
+ shared_path('bundle')
6
+ end
7
+
8
+ def bundler_installed?
9
+ ssh.run("which bundle").success?
10
+ end
11
+
12
+ def bundler_version
13
+ ssh.capture("bundle --version").split(' ').last
14
+ end
15
+
16
+ def install_bundler
17
+ res = ssh.run("gem install bundler")
18
+
19
+ if res.success?
20
+ log "Bundler installed: #{bundler_version}"
21
+ else
22
+ error "Bundler install failed: #{res.output}"
23
+ end
24
+ end
25
+
26
+ def bundle_install
27
+ log "Installing dependencies with Bundler"
28
+
29
+ cmd = [
30
+ "bundle install",
31
+ "--quiet",
32
+ "--path #{bundle_path}",
33
+ "--binstubs",
34
+ "--deployment"
35
+ ].join(' ')
36
+
37
+ res = ssh.run("cd #{release_path} && #{cmd}", &method(:stream_output))
38
+
39
+ unless res.success?
40
+ error "Unable to run bundle: #{res.output}"
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,7 @@
1
+ module Shuttle
2
+ module Support::Foreman
3
+ def foreman_installed?
4
+ ssh.run("which foreman").success?
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,59 @@
1
+ module Shuttle
2
+ module Support::Thin
3
+ def thin_config
4
+ config.thin || Hashr.new
5
+ end
6
+
7
+ def thin_host
8
+ thin_config.host || "127.0.0.1"
9
+ end
10
+
11
+ def thin_port
12
+ thin_config.port || "9000"
13
+ end
14
+
15
+ def thin_servers
16
+ thin_config.servers || 1
17
+ end
18
+
19
+ def thin_env
20
+ environment
21
+ end
22
+
23
+ def thin_options
24
+ [
25
+ "-a #{thin_host}",
26
+ "-p #{thin_port}",
27
+ "-e #{thin_env}",
28
+ "-s #{thin_servers}",
29
+ "-l #{shared_path('log/thin.log')}",
30
+ "-P #{shared_path('pids/thin.pid')}",
31
+ "-d"
32
+ ].join(' ')
33
+ end
34
+
35
+ def thin_start
36
+ log "Starting thin"
37
+
38
+ res = ssh.run("cd #{release_path} && ./bin/thin #{thin_options} start")
39
+
40
+ unless res.success?
41
+ error "Unable to start thin: #{res.output}"
42
+ end
43
+ end
44
+
45
+ def thin_stop
46
+ log "Stopping thin"
47
+
48
+ ssh.run("cd #{release_path} && ./bin/thin #{thin_options} stop")
49
+ end
50
+
51
+ def thin_restart
52
+ if ssh.file_exists?(shared_path("pids/thin.#{thin_port}.pid"))
53
+ thin_stop
54
+ end
55
+
56
+ thin_start
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,23 @@
1
+ module Shuttle
2
+ class Target
3
+ attr_reader :host, :user, :password
4
+ attr_reader :deploy_to
5
+
6
+ def initialize(hash)
7
+ @host = hash[:host]
8
+ @user = hash[:user]
9
+ @password = hash[:password]
10
+ @deploy_to = hash[:deploy_to]
11
+ end
12
+
13
+ def connection
14
+ @connection ||= Net::SSH::Session.new(host, user, password)
15
+ end
16
+
17
+ def validate!
18
+ raise Shuttle::ConfigError, "Host required" if host.nil?
19
+ raise Shuttle::ConfigError, "User required" if user.nil?
20
+ raise Shuttle::ConfigError, "Deploy path required" if deploy_to.nil?
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,264 @@
1
+ require 'uri'
2
+
3
+ module Shuttle
4
+ module Tasks
5
+ def setup
6
+ log "Preparing application structure"
7
+
8
+ execute_hook(:before_setup)
9
+
10
+ ssh.run "mkdir -p #{deploy_path}"
11
+ ssh.run "mkdir -p #{deploy_path('releases')}"
12
+ ssh.run "mkdir -p #{deploy_path('backups')}"
13
+ ssh.run "mkdir -p #{deploy_path('shared')}"
14
+ ssh.run "mkdir -p #{shared_path('tmp')}"
15
+ ssh.run "mkdir -p #{shared_path('pids')}"
16
+ ssh.run "mkdir -p #{shared_path('log')}"
17
+
18
+ execute_hook(:after_setup)
19
+ end
20
+
21
+ def deploy
22
+ setup
23
+ update_code
24
+ checkout_code
25
+ link_release
26
+ cleanup_releases
27
+ end
28
+
29
+ def keep_releases
30
+ config.app.keep_releases || 10
31
+ end
32
+
33
+ def update_code
34
+ if config.app.svn
35
+ return update_code_svn
36
+ end
37
+
38
+ error "Git is not installed" if !git_installed?
39
+ error "Git source url is not defined. Please define :git option first" if config.app.git.nil?
40
+
41
+ if ssh.directory_exists?(scm_path)
42
+ # Check if git remote has changed
43
+ current_remote = git_remote
44
+
45
+ if current_remote != config.app.git
46
+ log("Git remote change detected. Using #{config.app.git}", 'warning')
47
+
48
+ res = ssh.run("cd #{scm_path} && git remote rm origin && git remote add origin #{config.app.git}")
49
+ if res.failure?
50
+ error("Failed to change git remote: #{res.output}")
51
+ end
52
+ end
53
+
54
+ log "Fetching latest code"
55
+ res = ssh.run "cd #{scm_path} && git pull origin master"
56
+
57
+ if res.failure?
58
+ error "Unable to fetch latest code: #{res.output}"
59
+ end
60
+ else
61
+ log "Cloning repository #{config.app.git}"
62
+ res = ssh.run "cd #{deploy_path} && git clone --depth 25 --recursive --quiet #{config.app.git} scm"
63
+
64
+ if res.failure?
65
+ error "Failed to clone repository: #{res.output}"
66
+ end
67
+ end
68
+
69
+ branch = config.app.branch || 'master'
70
+
71
+ ssh.run("cd #{scm_path} && git fetch")
72
+
73
+ log "Using branch '#{branch}'"
74
+ result = ssh.run("cd #{scm_path} && git checkout -m #{branch}")
75
+
76
+ if result.failure?
77
+ error "Failed to checkout #{branch}: #{result.output}"
78
+ end
79
+
80
+ if ssh.file_exists?("#{scm_path}/.gitmodules")
81
+ log "Updating git submodules"
82
+ result = ssh.run("cd #{scm_path} && git submodule update --init --recursive")
83
+
84
+ if result.failure?
85
+ error "Failed to update submodules: #{result.output}"
86
+ end
87
+ end
88
+ end
89
+
90
+ def update_code_svn
91
+ error "Subversion is not installed" if !svn_installed?
92
+ error "Subversion source is not defined. Please define :svn option first" if config.app.svn.nil?
93
+
94
+ url = URI.parse(config.app.svn)
95
+ repo_url = "#{url.scheme}://#{url.host}#{url.path}"
96
+
97
+ opts = ["--non-interactive", "--quiet"]
98
+
99
+ if url.user
100
+ opts << "--username #{url.user}"
101
+ opts << "--password #{url.password}" if url.password
102
+ end
103
+
104
+ if ssh.directory_exists?(scm_path)
105
+ log "Fetching latest code"
106
+
107
+ res = ssh.run("cd #{scm_path} && svn up #{opts.join(' ')}")
108
+ if res.failure?
109
+ error "Unable to fetch latest code: #{res.output}"
110
+ end
111
+ else
112
+ log "Cloning repository #{config.app.svn}"
113
+ res = ssh.run("cd #{deploy_path} && svn checkout #{opts.join(' ')} #{repo_url} scm")
114
+
115
+ if res.failure?
116
+ error "Failed to clone repository: #{res.output}"
117
+ end
118
+ end
119
+ end
120
+
121
+ def checkout_code(path=nil)
122
+ checkout_path = [release_path, path].compact.join('/')
123
+ res = ssh.run("cp -a #{scm_path} #{checkout_path}")
124
+
125
+ if res.failure?
126
+ error "Failed to checkout code. Reason: #{res.output}"
127
+ else
128
+ ssh.run("cd #{release_path} && rm -rf $(find . | grep .git)")
129
+ ssh.run("cd #{release_path} && rm -rf $(find . -name .svn)")
130
+ end
131
+ end
132
+
133
+ def link_release
134
+ if !release_exists?
135
+ error "Release does not exist"
136
+ end
137
+
138
+ # Execute before link_release hook
139
+ execute_hook(:before_link_release)
140
+
141
+ log "Linking release"
142
+
143
+ # Check if `current` is a directory first
144
+ if ssh.run("unlink #{current_path}").failure?
145
+ ssh.run("rm -rf #{current_path}")
146
+ end
147
+
148
+ if ssh.run("ln -s #{release_path} #{current_path}").failure?
149
+ error "Unable to create symlink to current path"
150
+ end
151
+
152
+ ssh.run "echo #{version} > #{version_path}"
153
+
154
+ log "Release v#{version} has been deployed"
155
+
156
+ # Execute after link_release hook
157
+ execute_hook(:after_link_release)
158
+ end
159
+
160
+ def write_lock
161
+ ssh.run(%{echo #{deployer_hostname} > #{deploy_path}/.lock})
162
+ end
163
+
164
+ def release_lock
165
+ ssh.run("rm #{deploy_path}/.lock")
166
+ end
167
+
168
+ # Delete current session release
169
+ def cleanup_release
170
+ if ssh.directory_exists?(release_path)
171
+ ssh.run("rm -rf #{release_path}")
172
+ end
173
+ end
174
+
175
+ def cleanup_releases
176
+ ssh.run("cd #{deploy_path('releases')}")
177
+ ssh.run("count=`ls -1d [0-9]* | sort -rn | wc -l`")
178
+
179
+ count = ssh.capture("echo $count")
180
+
181
+ unless count.empty?
182
+ num = Integer(count) - Integer(keep_releases)
183
+
184
+ if num > 0
185
+ log "Cleaning up old releases: #{num}"
186
+
187
+ ssh.run("remove=$((count > #{keep_releases} ? count - #{keep_releases} : 0))")
188
+ ssh.run("ls -1d [0-9]* | sort -rn | tail -n $remove | xargs rm -rf {}")
189
+ end
190
+ end
191
+ end
192
+
193
+ def write_revision
194
+ if ssh.directory_exists?(deploy_path('scm'))
195
+ command = nil
196
+
197
+ if config.app.git
198
+ command = "git log --format='%H' -n 1"
199
+ elsif config.app.svn
200
+ command = "svn info |grep Revision: |cut -c11-"
201
+ end
202
+
203
+ if command
204
+ ssh.run("cd #{scm_path} && #{command} > #{release_path}/REVISION")
205
+ end
206
+ end
207
+ end
208
+
209
+ def export_environment
210
+ ssh.export_hash(
211
+ 'DEPLOY_APPLICATION' => config.app.name,
212
+ 'DEPLOY_USER' => target.user,
213
+ 'DEPLOY_PATH' => deploy_path,
214
+ 'DEPLOY_RELEASE_PATH' => release_path,
215
+ 'DEPLOY_CURRENT_PATH' => current_path,
216
+ 'DEPLOY_SHARED_PATH' => shared_path,
217
+ 'DEPLOY_SCM_PATH' => scm_path
218
+ )
219
+
220
+ if config.env?
221
+ log "Exporting environment variables"
222
+
223
+ config.env.each_pair do |k, v|
224
+ ssh.export(k, v)
225
+ end
226
+ end
227
+ end
228
+
229
+ def execute_hook(name)
230
+ if config.hooks && config.hooks[name]
231
+ execute_commands(config.hooks[name])
232
+ end
233
+ end
234
+
235
+ def deploy_running?
236
+ ssh.file_exists?("#{deploy_path}/.lock")
237
+ end
238
+
239
+ def connect
240
+ exec("ssh #{target.user}@#{target.host}")
241
+ end
242
+
243
+ def changes_at?(path)
244
+ result = ssh.run(%{diff -r #{current_path}/#{path} #{release_path}/#{path} 2>/dev/null})
245
+ result.success? ? false : true
246
+ end
247
+
248
+ def execute_commands(commands=[])
249
+ commands.flatten.compact.uniq.each do |cmd|
250
+ log %{Executing "#{cmd.strip}"}
251
+ command = cmd
252
+ command = "cd #{release_path} && #{command}" if ssh.directory_exists?(release_path)
253
+
254
+ result = ssh.run(command)
255
+
256
+ if result.failure?
257
+ error "Failed: #{result.output}"
258
+ else
259
+ stream_output(result.output)
260
+ end
261
+ end
262
+ end
263
+ end
264
+ end
@@ -0,0 +1,3 @@
1
+ module Shuttle
2
+ VERSION = '0.2.0.beta1'
3
+ end
data/lib/shuttle.rb ADDED
@@ -0,0 +1,35 @@
1
+ require 'terminal_helpers'
2
+ require 'net/ssh/session'
3
+ require 'chronic_duration'
4
+ require 'toml'
5
+ require 'hashr'
6
+ require 'yaml'
7
+ require 'safe_yaml'
8
+ require 'toml'
9
+ require 'digest/sha1'
10
+ require 'logger'
11
+
12
+ require 'shuttle/version'
13
+ require 'shuttle/errors'
14
+
15
+ module Shuttle
16
+ autoload :Session, 'shuttle/session'
17
+ autoload :Runner, 'shuttle/runner'
18
+ autoload :Deploy, 'shuttle/deploy'
19
+ autoload :Tasks, 'shuttle/tasks'
20
+ autoload :Target, 'shuttle/target'
21
+ autoload :Helpers, 'shuttle/helpers'
22
+
23
+ autoload :Static, 'shuttle/deployment/static'
24
+ autoload :Php, 'shuttle/deployment/php'
25
+ autoload :Wordpress, 'shuttle/deployment/wordpress'
26
+ autoload :Ruby, 'shuttle/deployment/ruby'
27
+ autoload :Rails, 'shuttle/deployment/rails'
28
+ autoload :Nodejs, 'shuttle/deployment/nodejs'
29
+
30
+ module Support
31
+ autoload :Bundler, 'shuttle/support/bundler'
32
+ autoload :Foreman, 'shuttle/support/foreman'
33
+ autoload :Thin, 'shuttle/support/thin'
34
+ end
35
+ end
@@ -0,0 +1,28 @@
1
+ require File.expand_path('../lib/shuttle/version', __FILE__)
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = "shuttle-deploy"
5
+ s.version = Shuttle::VERSION
6
+ s.summary = "Minimalistic deployment tool"
7
+ s.description = "Minimalistic deployment tool for small and one-server applications"
8
+ s.homepage = "https://github.com/sosedoff/shuttle"
9
+ s.authors = ["Dan Sosedoff"]
10
+ s.email = ["dan.sosedoff@gmail.com"]
11
+
12
+ s.add_development_dependency 'rake', '~> 10'
13
+ s.add_development_dependency 'rspec', '~> 2.13'
14
+ s.add_development_dependency 'simplecov', '~> 0.7'
15
+
16
+ s.add_dependency 'net-ssh', '~> 2.6'
17
+ s.add_dependency 'net-ssh-session', '~> 0.1'
18
+ s.add_dependency 'terminal_helpers', '~> 0.1'
19
+ s.add_dependency 'chronic_duration', '~> 0.9'
20
+ s.add_dependency 'hashr', '~> 0.0.22'
21
+ s.add_dependency 'safe_yaml', '~> 0.9'
22
+ s.add_dependency 'toml', '~> 0.0'
23
+
24
+ s.files = `git ls-files`.split("\n")
25
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
26
+ s.executables = `git ls-files -- bin/*`.split("\n").map{|f| File.basename(f)}
27
+ s.require_paths = ["lib"]
28
+ end
@@ -0,0 +1,4 @@
1
+ require 'spec_helper'
2
+
3
+ describe Shuttle::Deploy do
4
+ end
File without changes
@@ -0,0 +1,11 @@
1
+ app:
2
+ name: application
3
+ git: git@hostname.com:repo.git
4
+ strategy: static
5
+
6
+ targets:
7
+ staging:
8
+ host: localhost
9
+ user: deployer
10
+ password: password
11
+ deploy_to: /home/deployer