shuttle-deploy 0.2.0.beta1

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.
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