shuttle-deploy 0.2.0.beta1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +15 -0
- data/.gitignore +25 -0
- data/.magnum.yml +1 -0
- data/.rspec +2 -0
- data/Gemfile +3 -0
- data/LICENSE +18 -0
- data/README.md +230 -0
- data/Rakefile +10 -0
- data/bin/shuttle +60 -0
- data/examples/rails.yml +22 -0
- data/examples/static.yml +10 -0
- data/examples/wordpress.yml +34 -0
- data/lib/shuttle/config.rb +5 -0
- data/lib/shuttle/deploy.rb +58 -0
- data/lib/shuttle/deployment/nodejs.rb +48 -0
- data/lib/shuttle/deployment/php.rb +17 -0
- data/lib/shuttle/deployment/rails.rb +116 -0
- data/lib/shuttle/deployment/ruby.rb +40 -0
- data/lib/shuttle/deployment/static.rb +5 -0
- data/lib/shuttle/deployment/wordpress/cli.rb +27 -0
- data/lib/shuttle/deployment/wordpress/core.rb +50 -0
- data/lib/shuttle/deployment/wordpress/plugins.rb +112 -0
- data/lib/shuttle/deployment/wordpress/vip.rb +84 -0
- data/lib/shuttle/deployment/wordpress.rb +191 -0
- data/lib/shuttle/errors.rb +5 -0
- data/lib/shuttle/helpers.rb +50 -0
- data/lib/shuttle/runner.rb +153 -0
- data/lib/shuttle/session.rb +52 -0
- data/lib/shuttle/support/bundler.rb +45 -0
- data/lib/shuttle/support/foreman.rb +7 -0
- data/lib/shuttle/support/thin.rb +59 -0
- data/lib/shuttle/target.rb +23 -0
- data/lib/shuttle/tasks.rb +264 -0
- data/lib/shuttle/version.rb +3 -0
- data/lib/shuttle.rb +35 -0
- data/shuttle-deploy.gemspec +28 -0
- data/spec/deploy_spec.rb +4 -0
- data/spec/fixtures/.gitkeep +0 -0
- data/spec/fixtures/static.yml +11 -0
- data/spec/helpers_spec.rb +42 -0
- data/spec/spec_helper.rb +15 -0
- data/spec/target_spec.rb +41 -0
- 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,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
|
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
|
data/spec/deploy_spec.rb
ADDED
File without changes
|