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.
Files changed (72) hide show
  1. checksums.yaml +7 -0
  2. data/changelog.md +161 -0
  3. data/doc/config/file-permissions.md +33 -0
  4. data/doc/config/log-files.md +32 -0
  5. data/doc/config/task-definitions.md +88 -0
  6. data/doc/getting-started.md +96 -0
  7. data/doc/strategies/base.md +38 -0
  8. data/doc/strategies/branches.md +82 -0
  9. data/doc/strategies/releases.md +110 -0
  10. data/doc/strategies.md +17 -0
  11. data/lib/statistrano/config/configurable.rb +53 -0
  12. data/lib/statistrano/config/rake_task_with_context_creation.rb +43 -0
  13. data/lib/statistrano/config.rb +52 -0
  14. data/lib/statistrano/deployment/log_file.rb +44 -0
  15. data/lib/statistrano/deployment/manifest.rb +88 -0
  16. data/lib/statistrano/deployment/rake_tasks.rb +74 -0
  17. data/lib/statistrano/deployment/registerable.rb +11 -0
  18. data/lib/statistrano/deployment/releaser/revisions.rb +163 -0
  19. data/lib/statistrano/deployment/releaser/single.rb +48 -0
  20. data/lib/statistrano/deployment/releaser.rb +2 -0
  21. data/lib/statistrano/deployment/strategy/base.rb +132 -0
  22. data/lib/statistrano/deployment/strategy/branches/index/template.html.erb +78 -0
  23. data/lib/statistrano/deployment/strategy/branches/index.rb +40 -0
  24. data/lib/statistrano/deployment/strategy/branches/release.rb +73 -0
  25. data/lib/statistrano/deployment/strategy/branches.rb +198 -0
  26. data/lib/statistrano/deployment/strategy/check_git.rb +43 -0
  27. data/lib/statistrano/deployment/strategy/invoke_tasks.rb +58 -0
  28. data/lib/statistrano/deployment/strategy/releases.rb +76 -0
  29. data/lib/statistrano/deployment/strategy.rb +37 -0
  30. data/lib/statistrano/deployment.rb +10 -0
  31. data/lib/statistrano/log/default_logger.rb +105 -0
  32. data/lib/statistrano/log.rb +33 -0
  33. data/lib/statistrano/remote/file.rb +79 -0
  34. data/lib/statistrano/remote.rb +111 -0
  35. data/lib/statistrano/shell.rb +17 -0
  36. data/lib/statistrano/util/file_permissions.rb +34 -0
  37. data/lib/statistrano/util.rb +27 -0
  38. data/lib/statistrano/version.rb +3 -0
  39. data/lib/statistrano.rb +55 -0
  40. data/readme.md +247 -0
  41. data/spec/integration_tests/base_integration_spec.rb +103 -0
  42. data/spec/integration_tests/branches_integration_spec.rb +189 -0
  43. data/spec/integration_tests/releases/deploy_integration_spec.rb +116 -0
  44. data/spec/integration_tests/releases/list_releases_integration_spec.rb +38 -0
  45. data/spec/integration_tests/releases/prune_releases_integration_spec.rb +86 -0
  46. data/spec/integration_tests/releases/rollback_release_integration_spec.rb +46 -0
  47. data/spec/lib/statistrano/config/configurable_spec.rb +88 -0
  48. data/spec/lib/statistrano/config/rake_task_with_context_creation_spec.rb +73 -0
  49. data/spec/lib/statistrano/config_spec.rb +34 -0
  50. data/spec/lib/statistrano/deployment/log_file_spec.rb +75 -0
  51. data/spec/lib/statistrano/deployment/manifest_spec.rb +171 -0
  52. data/spec/lib/statistrano/deployment/rake_tasks_spec.rb +107 -0
  53. data/spec/lib/statistrano/deployment/registerable_spec.rb +19 -0
  54. data/spec/lib/statistrano/deployment/releaser/revisions_spec.rb +486 -0
  55. data/spec/lib/statistrano/deployment/releaser/single_spec.rb +59 -0
  56. data/spec/lib/statistrano/deployment/strategy/base_spec.rb +158 -0
  57. data/spec/lib/statistrano/deployment/strategy/branches_spec.rb +19 -0
  58. data/spec/lib/statistrano/deployment/strategy/check_git_spec.rb +39 -0
  59. data/spec/lib/statistrano/deployment/strategy/invoke_tasks_spec.rb +66 -0
  60. data/spec/lib/statistrano/deployment/strategy/releases_spec.rb +257 -0
  61. data/spec/lib/statistrano/deployment/strategy_spec.rb +76 -0
  62. data/spec/lib/statistrano/deployment_spec.rb +4 -0
  63. data/spec/lib/statistrano/log/default_logger_spec.rb +172 -0
  64. data/spec/lib/statistrano/log_spec.rb +36 -0
  65. data/spec/lib/statistrano/remote/file_spec.rb +166 -0
  66. data/spec/lib/statistrano/remote_spec.rb +226 -0
  67. data/spec/lib/statistrano/util/file_permissions_spec.rb +25 -0
  68. data/spec/lib/statistrano/util_spec.rb +23 -0
  69. data/spec/lib/statistrano_spec.rb +52 -0
  70. data/spec/spec_helper.rb +86 -0
  71. data/spec/support/given.rb +39 -0
  72. metadata +223 -0
@@ -0,0 +1,53 @@
1
+ module Statistrano
2
+ class Config
3
+ module Configurable
4
+
5
+ def configuration
6
+ @_configuration ||= Config.new()
7
+ end
8
+
9
+ def option key, value=nil, proc=nil
10
+ if proc
11
+ configuration.options[key] = { call: proc }
12
+ else
13
+ configuration.options[key] = value
14
+ end
15
+ end
16
+
17
+ def options *args
18
+ args.each { |a| option(a) }
19
+ end
20
+
21
+ def task name, method, description
22
+ configuration.tasks[name] = {
23
+ method: method,
24
+ desc: description
25
+ }
26
+ end
27
+
28
+ # add a config method to objects that
29
+ # extend Configurable
30
+ #
31
+ def self.extended extending_obj
32
+ extending_obj.send :define_method,
33
+ :config,
34
+ -> {
35
+ @_config ||= Config.new( self.class.configuration.options,
36
+ self.class.configuration.tasks )
37
+ }
38
+ end
39
+
40
+ # make sure that classes that inherit from
41
+ # classes that have been configured also inherit
42
+ # the configuration of the parent class
43
+ #
44
+ def inherited subclass
45
+ subclass.superclass.class_eval do
46
+ subclass.configuration.options.merge! subclass.superclass.configuration.options
47
+ subclass.configuration.tasks.merge! subclass.superclass.configuration.tasks
48
+ end
49
+ end
50
+
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,43 @@
1
+ module Statistrano
2
+ class Config
3
+
4
+ module RakeTaskWithContextCreation
5
+
6
+ def self.included base
7
+ base.module_eval do
8
+ def user_task_namespaces
9
+ @_user_task_namespaces ||= []
10
+ end
11
+
12
+ def user_tasks
13
+ @_user_tasks ||= []
14
+ end
15
+ end
16
+ end
17
+
18
+ def namespace namespace, &block
19
+ context = Context.new (user_task_namespaces + [namespace])
20
+ context.instance_eval &block
21
+ user_tasks.push *context.user_tasks
22
+ end
23
+
24
+ def task name, desc=nil, &block
25
+ task = { name: name,
26
+ namespaces: user_task_namespaces,
27
+ block: block }
28
+ task.merge!(desc: desc) if desc
29
+
30
+ user_tasks.push task
31
+ end
32
+
33
+ class Context
34
+ include RakeTaskWithContextCreation
35
+
36
+ def initialize namespaces=[]
37
+ @_user_task_namespaces = namespaces
38
+ end
39
+ end
40
+ end
41
+
42
+ end
43
+ end
@@ -0,0 +1,52 @@
1
+ require_relative 'config/configurable'
2
+ require_relative 'config/rake_task_with_context_creation'
3
+
4
+ module Statistrano
5
+ class Config
6
+ include RakeTaskWithContextCreation
7
+
8
+ attr_reader :options
9
+ attr_reader :tasks
10
+
11
+ # initalize with the potential for seed options
12
+ # this is required so that when config'd classes
13
+ # are extended we can pass that configuration along
14
+ def initialize options=nil, tasks=nil
15
+ @options = options.nil? ? {} : options.clone
16
+ @tasks = tasks.nil? ? {} : tasks.clone
17
+
18
+ @options.each do |key,val|
19
+ define_option_accessor key.to_sym
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ def define_option_accessor name
26
+ define_singleton_method(name) do |*args, &block|
27
+ if block
28
+ if args.first == :call
29
+ @options[name] = { call: block }
30
+ else
31
+ @options[name] = block
32
+ end
33
+ return
34
+ end
35
+
36
+ if args.length == 1
37
+ @options[name] = args[0]
38
+ elsif args.empty?
39
+ if @options[name].respond_to? :fetch
40
+ @options[name].fetch( :call, -> { @options[name] } ).call
41
+ else
42
+ @options[name]
43
+ end
44
+ else
45
+ raise ArgumentError, "wrong number of arguments (#{args.length} for 0..1)"
46
+ end
47
+ end
48
+ define_singleton_method("#{name}=") { |arg| @options[name] = arg }
49
+ end
50
+
51
+ end
52
+ end
@@ -0,0 +1,44 @@
1
+ module Statistrano
2
+ module Deployment
3
+ class LogFile
4
+
5
+ attr_reader :resolved_path, :remote, :file
6
+
7
+ def initialize path, remote
8
+ @remote = remote
9
+ @resolved_path = resolve_path path
10
+ @file = Remote::File.new @resolved_path, remote
11
+ end
12
+
13
+ def append! log_entry
14
+ file.append_content! log_entry.to_json
15
+ end
16
+
17
+ def last_entry
18
+ entries(1).last || {}
19
+ end
20
+
21
+ def tail length=10
22
+ entries(length)
23
+ end
24
+
25
+ private
26
+
27
+ def entries length=10
28
+ entries = file.content.split("\n") || []
29
+ entries.reverse[0...length].map do |e|
30
+ Util.symbolize_hash_keys( JSON.parse(e) )
31
+ end
32
+ end
33
+
34
+ def resolve_path path
35
+ if path.start_with? '/'
36
+ path
37
+ else
38
+ File.join( remote.config.remote_dir, path )
39
+ end
40
+ end
41
+
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,88 @@
1
+ module Statistrano
2
+ module Deployment
3
+ class Manifest
4
+
5
+ attr_reader :remote_dir, :remote, :file
6
+
7
+ def initialize remote_dir, remote
8
+ @remote_dir = remote_dir
9
+ @remote = remote
10
+ @file = Remote::File.new remote_path, remote
11
+ end
12
+
13
+ # return an array of records from the manifest
14
+ # they are processed to all have symbolized keys
15
+ #
16
+ def data
17
+ @_data ||= Array( JSON.parse(raw) ).map { |h| Util.symbolize_hash_keys(h) }
18
+ rescue JSON::ParserError => e
19
+ Log.error "manifest on #{remote.config.hostname} had invalid JSON\n",
20
+ e.message
21
+ end
22
+
23
+ # push data into the manifest array updating a
24
+ # record if it matches with the `match_key`
25
+ #
26
+ # not that if you have used the `push` method previously
27
+ # all duplicates with the matching key **will** be removed
28
+ #
29
+ def put data, match_key
30
+ remove_if { |i| i[match_key] == data[match_key] }
31
+ push data
32
+ end
33
+
34
+ # pushes a data has into the manifest's array
35
+ #
36
+ def push new_data
37
+ unless new_data.respond_to? :to_json
38
+ raise ArgumentError, "data must be serializable as JSON"
39
+ end
40
+
41
+ data << Util.symbolize_hash_keys(new_data)
42
+ end
43
+
44
+ # pass a condition to remove records from the
45
+ # manifest if it returns true
46
+ #
47
+ # example:
48
+ # to remove all records with the name "name"
49
+ #
50
+ # manifest.remove_if |release|
51
+ # release[:name] == "name"
52
+ # end
53
+ #
54
+ def remove_if &condition
55
+ data.delete_if do |item|
56
+ condition.call item
57
+ end
58
+ end
59
+
60
+ # update the manifest using the data
61
+ # currently stored on the object
62
+ #
63
+ def save!
64
+ file.update_content! serialize
65
+ end
66
+
67
+ private
68
+
69
+ def raw
70
+ content = file.content
71
+ if content.empty?
72
+ return "[]"
73
+ else
74
+ return content
75
+ end
76
+ end
77
+
78
+ def serialize data=data
79
+ data.to_json
80
+ end
81
+
82
+ def remote_path
83
+ File.join( remote_dir, 'manifest.json' )
84
+ end
85
+
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,74 @@
1
+ module Statistrano
2
+ module Deployment
3
+
4
+ module RakeTasks
5
+ class << self
6
+
7
+ # Add rake tasks
8
+ include ::Rake::DSL
9
+
10
+ # Register the rake tasks for the deployment
11
+ # @return [Void]
12
+ def register deployment
13
+ deployment.config.tasks.each do |task_name, task_attrs|
14
+ in_namespace rake_namespace(deployment) do
15
+ register_task deployment, task_name, task_attrs
16
+ end
17
+ end
18
+
19
+ deployment.config.user_tasks.each do |task_obj|
20
+ in_namespace rake_namespace(deployment) do
21
+ register_in_namespace_recursive deployment,
22
+ task_obj[:name],
23
+ task_obj[:desc],
24
+ task_obj[:namespaces],
25
+ task_obj[:block]
26
+ end
27
+ end
28
+ end
29
+
30
+ private
31
+
32
+ def rake_namespace deployment
33
+ deployment.name.to_sym
34
+ end
35
+
36
+ def in_namespace namespace, &block
37
+ namespace namespace do
38
+ yield
39
+ end
40
+ end
41
+
42
+ def register_task deployment, task_name, attrs={}
43
+ desc attrs[:desc]
44
+ task task_name do
45
+ deployment.public_send attrs[:method]
46
+ end
47
+ end
48
+
49
+ def register_in_namespace_recursive deployment, task_name, task_desc, task_space, block
50
+ if task_space.empty?
51
+ register_user_task deployment, task_name, task_desc, block
52
+ else
53
+ in_namespace task_space.first do
54
+ register_in_namespace_recursive deployment, task_name, task_desc, task_space[1..-1], block
55
+ end
56
+ end
57
+ end
58
+
59
+ def register_user_task deployment, task_name, task_desc, block
60
+ t = task task_name do
61
+ if block.arity == 1
62
+ block.call deployment
63
+ else
64
+ block.call
65
+ end
66
+ end
67
+ t.add_description(task_desc) if task_desc
68
+ end
69
+
70
+ end
71
+ end
72
+
73
+ end
74
+ end
@@ -0,0 +1,11 @@
1
+ module Statistrano
2
+ module Deployment
3
+
4
+ module Registerable
5
+ def register_strategy name
6
+ Strategy.register( self, name )
7
+ end
8
+ end
9
+
10
+ end
11
+ end
@@ -0,0 +1,163 @@
1
+ module Statistrano
2
+ module Deployment
3
+ module Releaser
4
+
5
+ class Revisions
6
+ include Strategy::InvokeTasks
7
+
8
+ attr_reader :release_name
9
+
10
+ def initialize
11
+ @release_name = Time.now.to_i.to_s
12
+ end
13
+
14
+ def create_release remote, build_data={}
15
+ setup_release_path remote
16
+ rsync_to_remote remote
17
+ invoke_pre_symlink_task remote
18
+ symlink_release remote
19
+ add_release_to_manifest remote, build_data
20
+ prune_releases remote
21
+ end
22
+
23
+ def setup_release_path remote
24
+ current_release = current_release remote
25
+ remote.create_remote_dir releases_path(remote)
26
+
27
+ if current_release.empty?
28
+ remote.create_remote_dir release_path(remote)
29
+ else
30
+ remote.run "cp -a #{release_path(remote, current_release)} #{release_path(remote)}"
31
+ end
32
+ end
33
+
34
+ def rsync_to_remote remote
35
+ resp = remote.rsync_to_remote local_path(remote), release_path(remote)
36
+ unless resp.success?
37
+ abort()
38
+ end
39
+ end
40
+
41
+ def symlink_release remote, release=nil
42
+ remote.run "ln -nfs #{release_path(remote, release)} #{public_path(remote)}"
43
+ end
44
+
45
+ def prune_releases remote
46
+ remove_untracked_releases remote
47
+ remove_releases_beyond_release_count remote
48
+ end
49
+
50
+ def list_releases remote
51
+ manifest = new_manifest remote
52
+ manifest.data.keep_if do |rel|
53
+ rel.has_key?(:release)
54
+ end.sort_by do |rel|
55
+ rel[:release]
56
+ end.reverse
57
+ end
58
+
59
+ # merge of manifest & log data (if a log)
60
+ def current_release_data remote
61
+ release_data = new_manifest(remote).data.last
62
+
63
+ if remote.config.log_file_path
64
+ release_data.merge! log_file(remote).last_entry
65
+ end
66
+
67
+ release_data
68
+ end
69
+
70
+ def rollback_release remote
71
+ manifest = new_manifest remote
72
+ releases = tracked_releases remote, manifest
73
+
74
+ unless releases.length > 1
75
+ return Log.error "There is only one release, best not to remove it"
76
+ end
77
+
78
+ symlink_release remote, releases[1]
79
+ remove_release releases[0], remote, manifest
80
+ manifest.save!
81
+ end
82
+
83
+ def add_release_to_manifest remote, build_data={}
84
+ manifest = new_manifest remote
85
+ manifest.push build_data.merge(release: release_name)
86
+ manifest.save!
87
+ end
88
+
89
+ private
90
+
91
+ def new_manifest remote
92
+ Deployment::Manifest.new remote.config.remote_dir, remote
93
+ end
94
+
95
+ def log_file remote
96
+ Deployment::LogFile.new remote.config.log_file_path, remote
97
+ end
98
+
99
+ def remove_releases_beyond_release_count remote
100
+ manifest = new_manifest remote
101
+ beyond = tracked_releases(remote, manifest)[remote.config.release_count..-1]
102
+ Array(beyond).each do |beyond|
103
+ remove_release beyond, remote, manifest
104
+ end
105
+ manifest.save!
106
+ end
107
+
108
+ def remove_release release_name, remote, manifest
109
+ if release_name == current_release(remote)
110
+ Log.warn "did not remove release '#{release_name}' because it is current"
111
+ return
112
+ end
113
+
114
+ manifest.remove_if { |r| r[:release] == release_name }
115
+ remote.run("rm -rf #{File.join(releases_path(remote), release_name)}")
116
+ end
117
+
118
+ def remove_untracked_releases remote
119
+ manifest = new_manifest remote
120
+ (remote_releases(remote) - tracked_releases(remote)).each do |untracked_release|
121
+ remove_release untracked_release, remote, manifest
122
+ end
123
+ end
124
+
125
+ def current_release remote
126
+ resp = remote.run("readlink #{public_path(remote)}")
127
+ resp.stdout.sub( /#{releases_path(remote)}\/?/, '' ).strip
128
+ end
129
+
130
+ def remote_releases remote
131
+ remote.run("ls -m #{releases_path(remote)}").stdout
132
+ .split(',').map(&:strip)
133
+ end
134
+
135
+ def tracked_releases remote, manifest=nil
136
+ manifest ||= new_manifest remote
137
+ manifest.data.map do |data|
138
+ data.fetch(:release, nil)
139
+ end.compact.sort.reverse
140
+ end
141
+
142
+ def local_path remote=nil
143
+ File.join( Dir.pwd, remote.config.local_dir )
144
+ end
145
+
146
+ def releases_path remote=nil
147
+ File.join( remote.config.remote_dir, remote.config.release_dir )
148
+ end
149
+
150
+ def release_path remote=nil, release=nil
151
+ release ||= release_name
152
+ File.join( releases_path(remote), release )
153
+ end
154
+
155
+ def public_path remote=nil
156
+ File.join( remote.config.remote_dir, remote.config.public_dir )
157
+ end
158
+
159
+ end
160
+
161
+ end
162
+ end
163
+ end
@@ -0,0 +1,48 @@
1
+ module Statistrano
2
+ module Deployment
3
+ module Releaser
4
+
5
+ class Single
6
+
7
+ attr_reader :release_name
8
+
9
+ def initialize
10
+ @release_name = Time.now.to_i.to_s
11
+ end
12
+
13
+ def create_release remote, build_data={}
14
+ setup remote
15
+ rsync_to_remote remote
16
+ end
17
+
18
+ def setup remote
19
+ Log.info "Setting up the remote"
20
+ remote.run "mkdir -p #{remote_path(remote)}"
21
+ end
22
+
23
+ def rsync_to_remote remote
24
+ resp = remote.rsync_to_remote local_path(remote), remote_path(remote)
25
+ unless resp.success?
26
+ abort()
27
+ end
28
+ end
29
+
30
+ private
31
+
32
+ def local_path remote
33
+ File.join( Dir.pwd, remote.config.local_dir )
34
+ end
35
+
36
+ def remote_path remote=nil
37
+ if remote.config.respond_to?(:public_dir) && remote.config.public_dir
38
+ return File.join( remote.config.remote_dir, remote.config.public_dir )
39
+ else
40
+ return remote.config.remote_dir
41
+ end
42
+ end
43
+
44
+ end
45
+
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,2 @@
1
+ require_relative 'releaser/single'
2
+ require_relative 'releaser/revisions'