statistrano 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
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'