statistrano 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/changelog.md +161 -0
- data/doc/config/file-permissions.md +33 -0
- data/doc/config/log-files.md +32 -0
- data/doc/config/task-definitions.md +88 -0
- data/doc/getting-started.md +96 -0
- data/doc/strategies/base.md +38 -0
- data/doc/strategies/branches.md +82 -0
- data/doc/strategies/releases.md +110 -0
- data/doc/strategies.md +17 -0
- data/lib/statistrano/config/configurable.rb +53 -0
- data/lib/statistrano/config/rake_task_with_context_creation.rb +43 -0
- data/lib/statistrano/config.rb +52 -0
- data/lib/statistrano/deployment/log_file.rb +44 -0
- data/lib/statistrano/deployment/manifest.rb +88 -0
- data/lib/statistrano/deployment/rake_tasks.rb +74 -0
- data/lib/statistrano/deployment/registerable.rb +11 -0
- data/lib/statistrano/deployment/releaser/revisions.rb +163 -0
- data/lib/statistrano/deployment/releaser/single.rb +48 -0
- data/lib/statistrano/deployment/releaser.rb +2 -0
- data/lib/statistrano/deployment/strategy/base.rb +132 -0
- data/lib/statistrano/deployment/strategy/branches/index/template.html.erb +78 -0
- data/lib/statistrano/deployment/strategy/branches/index.rb +40 -0
- data/lib/statistrano/deployment/strategy/branches/release.rb +73 -0
- data/lib/statistrano/deployment/strategy/branches.rb +198 -0
- data/lib/statistrano/deployment/strategy/check_git.rb +43 -0
- data/lib/statistrano/deployment/strategy/invoke_tasks.rb +58 -0
- data/lib/statistrano/deployment/strategy/releases.rb +76 -0
- data/lib/statistrano/deployment/strategy.rb +37 -0
- data/lib/statistrano/deployment.rb +10 -0
- data/lib/statistrano/log/default_logger.rb +105 -0
- data/lib/statistrano/log.rb +33 -0
- data/lib/statistrano/remote/file.rb +79 -0
- data/lib/statistrano/remote.rb +111 -0
- data/lib/statistrano/shell.rb +17 -0
- data/lib/statistrano/util/file_permissions.rb +34 -0
- data/lib/statistrano/util.rb +27 -0
- data/lib/statistrano/version.rb +3 -0
- data/lib/statistrano.rb +55 -0
- data/readme.md +247 -0
- data/spec/integration_tests/base_integration_spec.rb +103 -0
- data/spec/integration_tests/branches_integration_spec.rb +189 -0
- data/spec/integration_tests/releases/deploy_integration_spec.rb +116 -0
- data/spec/integration_tests/releases/list_releases_integration_spec.rb +38 -0
- data/spec/integration_tests/releases/prune_releases_integration_spec.rb +86 -0
- data/spec/integration_tests/releases/rollback_release_integration_spec.rb +46 -0
- data/spec/lib/statistrano/config/configurable_spec.rb +88 -0
- data/spec/lib/statistrano/config/rake_task_with_context_creation_spec.rb +73 -0
- data/spec/lib/statistrano/config_spec.rb +34 -0
- data/spec/lib/statistrano/deployment/log_file_spec.rb +75 -0
- data/spec/lib/statistrano/deployment/manifest_spec.rb +171 -0
- data/spec/lib/statistrano/deployment/rake_tasks_spec.rb +107 -0
- data/spec/lib/statistrano/deployment/registerable_spec.rb +19 -0
- data/spec/lib/statistrano/deployment/releaser/revisions_spec.rb +486 -0
- data/spec/lib/statistrano/deployment/releaser/single_spec.rb +59 -0
- data/spec/lib/statistrano/deployment/strategy/base_spec.rb +158 -0
- data/spec/lib/statistrano/deployment/strategy/branches_spec.rb +19 -0
- data/spec/lib/statistrano/deployment/strategy/check_git_spec.rb +39 -0
- data/spec/lib/statistrano/deployment/strategy/invoke_tasks_spec.rb +66 -0
- data/spec/lib/statistrano/deployment/strategy/releases_spec.rb +257 -0
- data/spec/lib/statistrano/deployment/strategy_spec.rb +76 -0
- data/spec/lib/statistrano/deployment_spec.rb +4 -0
- data/spec/lib/statistrano/log/default_logger_spec.rb +172 -0
- data/spec/lib/statistrano/log_spec.rb +36 -0
- data/spec/lib/statistrano/remote/file_spec.rb +166 -0
- data/spec/lib/statistrano/remote_spec.rb +226 -0
- data/spec/lib/statistrano/util/file_permissions_spec.rb +25 -0
- data/spec/lib/statistrano/util_spec.rb +23 -0
- data/spec/lib/statistrano_spec.rb +52 -0
- data/spec/spec_helper.rb +86 -0
- data/spec/support/given.rb +39 -0
- metadata +223 -0
@@ -0,0 +1,37 @@
|
|
1
|
+
module Statistrano
|
2
|
+
module Deployment
|
3
|
+
module Strategy
|
4
|
+
|
5
|
+
class << self
|
6
|
+
|
7
|
+
def registered
|
8
|
+
@_registered ||= {}
|
9
|
+
end
|
10
|
+
|
11
|
+
def register deployment, name
|
12
|
+
registered[name.to_sym] = deployment
|
13
|
+
end
|
14
|
+
|
15
|
+
def find name
|
16
|
+
registered.fetch(name.to_sym) do
|
17
|
+
raise UndefinedStrategy, "no strategies are registered as :#{name}"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
|
23
|
+
class UndefinedStrategy < StandardError
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# strategy utils
|
31
|
+
require_relative 'strategy/invoke_tasks'
|
32
|
+
require_relative 'strategy/check_git'
|
33
|
+
|
34
|
+
# strategies
|
35
|
+
require_relative 'strategy/base'
|
36
|
+
require_relative 'strategy/branches'
|
37
|
+
require_relative 'strategy/releases'
|
@@ -0,0 +1,10 @@
|
|
1
|
+
# deployment utils
|
2
|
+
require_relative 'deployment/manifest'
|
3
|
+
require_relative 'deployment/log_file'
|
4
|
+
require_relative 'deployment/rake_tasks'
|
5
|
+
require_relative 'deployment/registerable'
|
6
|
+
|
7
|
+
|
8
|
+
# deployment types
|
9
|
+
require_relative 'deployment/strategy'
|
10
|
+
require_relative 'deployment/releaser'
|
@@ -0,0 +1,105 @@
|
|
1
|
+
module Statistrano
|
2
|
+
class Log
|
3
|
+
|
4
|
+
|
5
|
+
# Error, Warning and Message Logging
|
6
|
+
class DefaultLogger
|
7
|
+
|
8
|
+
def info *msg
|
9
|
+
status, msg = extract_status "", *msg
|
10
|
+
|
11
|
+
case status
|
12
|
+
when :success then
|
13
|
+
color = :green
|
14
|
+
else
|
15
|
+
color = :bright
|
16
|
+
end
|
17
|
+
|
18
|
+
to_stdout status, color, *msg
|
19
|
+
end
|
20
|
+
alias_method :debug, :info
|
21
|
+
|
22
|
+
def warn *msg
|
23
|
+
status, msg = extract_status "warning", *msg
|
24
|
+
to_stdout status, :yellow, *msg
|
25
|
+
end
|
26
|
+
|
27
|
+
def error *msg
|
28
|
+
status, msg = extract_status "error", *msg
|
29
|
+
to_stderr status, :red, *msg
|
30
|
+
end
|
31
|
+
alias_method :fatal, :error
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def extract_status default, *msg
|
36
|
+
if msg.first.is_a? Symbol
|
37
|
+
status = msg.shift
|
38
|
+
else
|
39
|
+
status = default
|
40
|
+
end
|
41
|
+
[status, msg]
|
42
|
+
end
|
43
|
+
|
44
|
+
def to_stdout status, color, *msg
|
45
|
+
$stdout.puts "#{Formatter.new(status, color, *msg).output}"
|
46
|
+
$stdout.flush
|
47
|
+
end
|
48
|
+
|
49
|
+
def to_stderr status, color, *msg
|
50
|
+
$stderr.puts "#{Formatter.new(status, color, *msg).output}"
|
51
|
+
$stderr.flush
|
52
|
+
end
|
53
|
+
|
54
|
+
class Formatter
|
55
|
+
attr_reader :width, :status, :color, :msgs
|
56
|
+
|
57
|
+
def initialize status, color, *msg
|
58
|
+
@width = 14
|
59
|
+
@status = status.to_s
|
60
|
+
@color = color
|
61
|
+
@msgs = msg
|
62
|
+
end
|
63
|
+
|
64
|
+
def output
|
65
|
+
Rainbow(anchor).bright + padding + Rainbow(status).public_send(color) + formatted_messages
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
|
70
|
+
def anchor
|
71
|
+
"-> "
|
72
|
+
end
|
73
|
+
|
74
|
+
def padding
|
75
|
+
num = (width - status.length)
|
76
|
+
|
77
|
+
if num < 0
|
78
|
+
@width = status.length + 1
|
79
|
+
return spaces(0)
|
80
|
+
else
|
81
|
+
return spaces num
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def spaces num
|
86
|
+
Array.new(num).join(" ")
|
87
|
+
end
|
88
|
+
|
89
|
+
def formatted_messages
|
90
|
+
messages = []
|
91
|
+
msgs.each_with_index do |msg, idx|
|
92
|
+
if idx == 0
|
93
|
+
messages << " #{msg}"
|
94
|
+
else
|
95
|
+
messages << "#{spaces( anchor.length + width )} #{msg}"
|
96
|
+
end
|
97
|
+
end
|
98
|
+
messages.join("\n")
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
end
|
103
|
+
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
require_relative 'log/default_logger'
|
3
|
+
|
4
|
+
module Statistrano
|
5
|
+
|
6
|
+
# interface should match the ruby logger
|
7
|
+
# so we will implement:
|
8
|
+
#
|
9
|
+
# => fatal
|
10
|
+
# => error
|
11
|
+
# => warn
|
12
|
+
# => info
|
13
|
+
# => debug
|
14
|
+
#
|
15
|
+
# note that DefaultLogger does accept multiline logs
|
16
|
+
# as *args so you will need a wrapper for some logging libraries
|
17
|
+
|
18
|
+
class Log
|
19
|
+
extend SingleForwardable
|
20
|
+
def_delegators :logger_instance, :fatal, :error, :warn, :info, :debug
|
21
|
+
|
22
|
+
class << self
|
23
|
+
def set_logger logger
|
24
|
+
@_logger = logger
|
25
|
+
end
|
26
|
+
|
27
|
+
def logger_instance
|
28
|
+
@_logger ||= DefaultLogger.new
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
module Statistrano
|
2
|
+
class Remote
|
3
|
+
|
4
|
+
class File
|
5
|
+
|
6
|
+
attr_reader :path, :remote, :permissions
|
7
|
+
|
8
|
+
def initialize path, remote, permissions=644
|
9
|
+
@path = path
|
10
|
+
@remote = remote
|
11
|
+
@permissions = permissions
|
12
|
+
end
|
13
|
+
|
14
|
+
def content
|
15
|
+
resp = remote.run "cat #{path}"
|
16
|
+
if resp.success?
|
17
|
+
resp.stdout
|
18
|
+
else
|
19
|
+
""
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def update_content! new_content
|
24
|
+
create_remote_file unless remote_file_exists?
|
25
|
+
resp = remote.run "echo '#{new_content}' > #{path}"
|
26
|
+
|
27
|
+
if resp.success?
|
28
|
+
Log.info :success, "file at #{path} on #{remote.config.hostname} saved"
|
29
|
+
else
|
30
|
+
Log.error "problem saving the file #{path} on #{remote.config.hostname}",
|
31
|
+
resp.stderr
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def append_content! new_content
|
36
|
+
create_remote_file unless remote_file_exists?
|
37
|
+
resp = remote.run "echo '#{new_content}' >> #{path}"
|
38
|
+
|
39
|
+
if resp.success?
|
40
|
+
Log.info :success, "appended content to file at #{path} on #{remote.config.hostname}"
|
41
|
+
else
|
42
|
+
Log.error "problem appending content to file at #{path} on #{remote.config.hostname}",
|
43
|
+
resp.stderr
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def destroy!
|
48
|
+
resp = remote.run "rm #{path}"
|
49
|
+
if resp.success?
|
50
|
+
Log.info :success, "file at #{path} on #{remote.config.hostname} removed"
|
51
|
+
else
|
52
|
+
Log.error "failed to remove #{path} on #{remote.config.hostname}",
|
53
|
+
resp.stderr
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
def remote_file_exists?
|
60
|
+
resp = remote.run "[ -f #{path} ] && echo \"exists\""
|
61
|
+
resp.success? && resp.stdout.strip == "exists"
|
62
|
+
end
|
63
|
+
|
64
|
+
def create_remote_file
|
65
|
+
resp = remote.run "touch #{path} " +
|
66
|
+
"&& chmod #{permissions} #{path}"
|
67
|
+
|
68
|
+
if resp.success?
|
69
|
+
Log.info :success, "created file at #{path} on #{remote.config.hostname}"
|
70
|
+
else
|
71
|
+
Log.error "problem creating file at #{path} on #{remote.config.hostname}",
|
72
|
+
resp.stderr
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
require_relative 'remote/file'
|
2
|
+
|
3
|
+
module Statistrano
|
4
|
+
|
5
|
+
# a remote is a databag of config specific for an
|
6
|
+
# individual target for deployment
|
7
|
+
# including it's own ssh connection to it's target server
|
8
|
+
class Remote
|
9
|
+
|
10
|
+
attr_reader :config
|
11
|
+
|
12
|
+
def initialize config
|
13
|
+
@config = config
|
14
|
+
raise ArgumentError, "a hostname is required" unless config.hostname
|
15
|
+
end
|
16
|
+
|
17
|
+
def test_connection
|
18
|
+
Log.info "testing connection to #{config.hostname}"
|
19
|
+
|
20
|
+
resp = run 'whoami'
|
21
|
+
done
|
22
|
+
|
23
|
+
if resp.success?
|
24
|
+
Log.info "#{config.hostname} says \"Hello #{resp.stdout.strip}\""
|
25
|
+
return true
|
26
|
+
else
|
27
|
+
Log.error "connection failed for #{config.hostname}",
|
28
|
+
resp.stderr
|
29
|
+
return false
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def run command
|
34
|
+
if config.verbose
|
35
|
+
Log.info :"#{config.hostname}", "running cmd: #{command}"
|
36
|
+
end
|
37
|
+
|
38
|
+
session.run command
|
39
|
+
end
|
40
|
+
|
41
|
+
def done
|
42
|
+
session.close_session
|
43
|
+
end
|
44
|
+
|
45
|
+
def create_remote_dir path
|
46
|
+
unless path[0] == "/"
|
47
|
+
raise ArgumentError, "path must be absolute"
|
48
|
+
end
|
49
|
+
|
50
|
+
Log.info "Setting up directory at '#{path}' on #{config.hostname}"
|
51
|
+
resp = run "mkdir -p -m #{config.dir_permissions} #{path}"
|
52
|
+
unless resp.success?
|
53
|
+
Log.error "Unable to create directory '#{path}' on #{config.hostname}",
|
54
|
+
resp.stderr
|
55
|
+
abort()
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def rsync_to_remote local_path, remote_path
|
60
|
+
local_path = local_path.chomp("/")
|
61
|
+
remote_path = remote_path.chomp("/")
|
62
|
+
|
63
|
+
Log.info "Syncing files from '#{local_path}' to '#{remote_path}' on #{config.hostname}"
|
64
|
+
|
65
|
+
time_before = Time.now
|
66
|
+
resp = Shell.run_local "rsync #{rsync_options} " +
|
67
|
+
"-e ssh #{local_path}/ " +
|
68
|
+
"#{host_connection}:#{remote_path}/"
|
69
|
+
time_after = Time.now
|
70
|
+
total_time = (time_after - time_before).round(2)
|
71
|
+
|
72
|
+
if resp.success?
|
73
|
+
Log.info :success, "Files synced to remote on #{config.hostname} in #{total_time}s"
|
74
|
+
else
|
75
|
+
Log.error "Error syncing files to remote on #{config.hostname}",
|
76
|
+
resp.stderr
|
77
|
+
end
|
78
|
+
|
79
|
+
resp
|
80
|
+
end
|
81
|
+
|
82
|
+
private
|
83
|
+
|
84
|
+
def session
|
85
|
+
@_ssh_session ||= HereOrThere::Remote.session ssh_options
|
86
|
+
end
|
87
|
+
|
88
|
+
def ssh_options
|
89
|
+
ssh_options = { hostname: config.hostname }
|
90
|
+
[ :user, :password, :keys, :forward_agent ].each do |key|
|
91
|
+
ssh_options[key] = config.public_send(key) if config.public_send(key)
|
92
|
+
end
|
93
|
+
|
94
|
+
return ssh_options
|
95
|
+
end
|
96
|
+
|
97
|
+
def host_connection
|
98
|
+
config.user ? "#{config.user}@#{config.hostname}" : config.hostname
|
99
|
+
end
|
100
|
+
|
101
|
+
def rsync_options
|
102
|
+
dir_perms = Util::FilePermissions.new( config.dir_permissions ).to_chmod
|
103
|
+
file_perms = Util::FilePermissions.new( config.file_permissions ).to_chmod
|
104
|
+
|
105
|
+
"#{config.rsync_flags} --chmod=" +
|
106
|
+
"Du=#{dir_perms.user},Dg=#{dir_perms.group},Do=#{dir_perms.others}," +
|
107
|
+
"Fu=#{file_perms.user},Fg=#{file_perms.group},Fo=#{file_perms.others}"
|
108
|
+
end
|
109
|
+
|
110
|
+
end
|
111
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Statistrano
|
2
|
+
module Util
|
3
|
+
|
4
|
+
class FilePermissions
|
5
|
+
|
6
|
+
attr_reader :user, :group, :others
|
7
|
+
|
8
|
+
def initialize int
|
9
|
+
@user, @group, @others = int.to_s.chars.to_a
|
10
|
+
end
|
11
|
+
|
12
|
+
def to_chmod
|
13
|
+
Struct.new(:user, :group, :others)
|
14
|
+
.new( chmod_map(user), chmod_map(group), chmod_map(others) )
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def chmod_map num
|
20
|
+
{
|
21
|
+
"7" => "rwx",
|
22
|
+
"6" => "rw",
|
23
|
+
"5" => "rx",
|
24
|
+
"4" => "r",
|
25
|
+
"3" => "wx",
|
26
|
+
"2" => "w",
|
27
|
+
"1" => "x",
|
28
|
+
"0" => "-"
|
29
|
+
}.fetch num
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require_relative 'util/file_permissions'
|
2
|
+
|
3
|
+
module Statistrano
|
4
|
+
module Util
|
5
|
+
|
6
|
+
class << self
|
7
|
+
|
8
|
+
def symbolize_hash_keys hash
|
9
|
+
hash.inject({}) do |out, (key, value)|
|
10
|
+
k = case key
|
11
|
+
when String then key.to_sym
|
12
|
+
else key
|
13
|
+
end
|
14
|
+
v = case value
|
15
|
+
when Hash then symbolize_hash_keys(value)
|
16
|
+
when Array then value.map { |h| symbolize_hash_keys(h) }
|
17
|
+
else value
|
18
|
+
end
|
19
|
+
out[k] = v
|
20
|
+
out
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
end
|
data/lib/statistrano.rb
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
# stdlib
|
2
|
+
require 'json'
|
3
|
+
require 'forwardable'
|
4
|
+
require 'fileutils'
|
5
|
+
require 'benchmark'
|
6
|
+
|
7
|
+
# libraries
|
8
|
+
require 'rainbow'
|
9
|
+
require 'rake'
|
10
|
+
require 'slugity/extend_string'
|
11
|
+
require 'here_or_there'
|
12
|
+
require 'asgit'
|
13
|
+
|
14
|
+
# utility modules
|
15
|
+
require_relative 'statistrano/util'
|
16
|
+
require_relative 'statistrano/shell'
|
17
|
+
require_relative 'statistrano/log'
|
18
|
+
|
19
|
+
# deployment modules
|
20
|
+
require_relative 'statistrano/config'
|
21
|
+
require_relative 'statistrano/remote'
|
22
|
+
require_relative 'statistrano/deployment'
|
23
|
+
|
24
|
+
|
25
|
+
# DSL for defining deployments of static files
|
26
|
+
#
|
27
|
+
# == Define a server
|
28
|
+
#
|
29
|
+
# define_deployment "foo" do |config|
|
30
|
+
# config.attribute = value
|
31
|
+
# end
|
32
|
+
#
|
33
|
+
module Statistrano::DSL
|
34
|
+
|
35
|
+
# Define a deployment
|
36
|
+
# @param [String] name of the deployment
|
37
|
+
# @param [Symbol] type of deployment
|
38
|
+
# @return [Statistrano::Deployment::Base]
|
39
|
+
def define_deployment name, type=:base, &block
|
40
|
+
deployment = ::Statistrano::Deployment::Strategy.find(type).new( name )
|
41
|
+
|
42
|
+
if block_given?
|
43
|
+
if block.arity == 1
|
44
|
+
yield deployment.config
|
45
|
+
else
|
46
|
+
deployment.config.instance_eval &block
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
return deployment
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
|
55
|
+
include Statistrano::DSL
|