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