statistrano 1.2.0
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 +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
|