capnotify 0.1.0pre

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.
@@ -0,0 +1,121 @@
1
+ require 'capnotify/version'
2
+ require 'pry'
3
+
4
+ module Capnotify
5
+ module Plugin
6
+
7
+ def print_splash
8
+ return if fetch(:capnotify_hide_splash, false)
9
+
10
+ puts <<-SPLASH
11
+ __________________
12
+ - --|\\ Deployment /| _____ __ _ ___
13
+ - ---| \\ Complete / | / ___/__ ____ ___ ___ / /_(_) _/_ __
14
+ - ----| /\\____________/\\ | / /__/ _ `/ _ \\/ _ \\/ _ \\/ __/ / _/ // /
15
+ - -----|/ - Capistrano - \\| \\___/\\_,_/ .__/_//_/\\___/\\__/_/_/ \\_, /
16
+ - ------|__________________| /_/ /___/
17
+
18
+ SPLASH
19
+ end
20
+
21
+ # convenience method for getting the friendly app name
22
+ # If the stage is specified (the deployment is using multistage), include that.
23
+ # given that the application is "MyApp" and the stage is "production", this will return "MyApp production"
24
+ def appname
25
+ fetch(:capnotify_appname, "")
26
+ end
27
+
28
+ def load_plugin(name, mod)
29
+ Capistrano.plugin name, mod
30
+
31
+ get_plugin(name).init
32
+ end
33
+
34
+ def unload_plugin(name)
35
+ p = get_plugin(name)
36
+
37
+ p.unload if p.respond_to?(:unload)
38
+ Capistrano.remove_plugin(name)
39
+ end
40
+
41
+ def get_plugin(name)
42
+ raise "Unknown plugin: #{ name }" unless Capistrano::EXTENSIONS.keys.include?(name)
43
+ self.send(name)
44
+ end
45
+ private :get_plugin
46
+
47
+ # template stuff:
48
+
49
+ # return the path to the built-in template with the given name
50
+ def built_in_template_for(template_name)
51
+ File.join( File.dirname(__FILE__), 'templates', template_name )
52
+ end
53
+
54
+ # given a path to an ERB template, process it with the current binding and return the output.
55
+ def build_template(template_path)
56
+ # FIXME: this is called every time build_template is called.
57
+ # although this is idepodent, it's got room for optimization
58
+ self.build_components!
59
+
60
+ ERB.new( File.open( template_path ).read, nil, '<>' ).result(self.binding)
61
+ end
62
+
63
+ # component stuff
64
+
65
+ # returns the capnotify_component_list
66
+ # this is the underlying mechanism for working with components
67
+ # append or prepend or insert from here.
68
+ def components
69
+ fetch(:capnotify_component_list)
70
+ end
71
+
72
+ # fetch a component given the name
73
+ # this is most useful for getting a component directly if you want to make modificatins to it
74
+ def component(name)
75
+ components.each { |c| return c if c.name == name.to_sym }
76
+ return nil
77
+ end
78
+
79
+ # insert the given component before the component with `name`
80
+ # if no component is found with that name, the component will be inserted at the end
81
+ def insert_component_before(name, component)
82
+ # iterate over all components, find the component with the given name
83
+ # once found, insert the given component at that location and return
84
+ components.each_with_index do |c, i|
85
+ if c.name == name
86
+ components.insert(i, component)
87
+ return
88
+ end
89
+ end
90
+
91
+ components << component
92
+ end
93
+
94
+ # insert the given component after the component with `name`
95
+ # if no component is found with that name, the component will be inserted at the end
96
+ def insert_component_after(name, component)
97
+ # iterate over all components, find the component with the given name
98
+ # once found, insert the given component at the following location and return
99
+ components.each_with_index do |c, i|
100
+ if c.name == name
101
+ components.insert(i + 1, component)
102
+ return
103
+ end
104
+ end
105
+
106
+ components << component
107
+ end
108
+
109
+ # delete the component with the given name
110
+ # return the remaining list of components (to enable chaining)
111
+ def delete_component(name)
112
+ components.delete_if { |c| c.name == name.to_sym }
113
+ end
114
+
115
+ # build all components
116
+ def build_components!
117
+ set :capnotify_component_list, self.components.map { |c| c.build!(self) }
118
+ end
119
+
120
+ end
121
+ end
@@ -0,0 +1,20 @@
1
+ <% if @content.is_a? Hash %>
2
+ <dl>
3
+ <% @content.each do |k,v| %>
4
+ <dt><%= k %></dt>
5
+ <dd><%= v %></dd>
6
+ <% end %>
7
+ </dl>
8
+ <% elsif @content.is_a? Array %>
9
+ <ul>
10
+ <% @content.each do |row| %>
11
+ <li>
12
+ <%= row %>
13
+ </li>
14
+ <% end %>
15
+ </ul>
16
+ <% elsif @content.is_a? String %>
17
+ <div class="content">
18
+ <%= @content %>
19
+ </div>
20
+ <% end %>
@@ -0,0 +1,11 @@
1
+ <% if @content.is_a? Hash %>
2
+ <% @content.each do |k,v| %>
3
+ <%= k %>: <%= v %>
4
+ <% end %>
5
+ <% elsif @content.is_a? Array %>
6
+ <% @content.each do |row| %>
7
+ <%= row %>
8
+ <% end %>
9
+ <% elsif @content.is_a? String %>
10
+ <%= @content %>
11
+ <% end %>
@@ -0,0 +1,78 @@
1
+ <html>
2
+ <head>
3
+ <style>
4
+ body {
5
+ font: 12px "Helvetica", "Lucida Grande", "Trebuchet MS", Verdana, sans-serif;
6
+ }
7
+ .section h2 {
8
+ color: #999;
9
+ margin: 0;
10
+ }
11
+
12
+ .section {
13
+ margin-bottom: 10px;
14
+ border-radius: 10px;
15
+ background-color: #eee;
16
+
17
+ padding: 5px 20px;
18
+ }
19
+
20
+ .content {
21
+ margin-top: 10px;
22
+ margin-bottom: 10px;
23
+ }
24
+
25
+ dt {
26
+ width: 15%;
27
+ margin-right: 10px;
28
+ display: inline-block;
29
+ white-space: nowrap;
30
+
31
+ clear: both;
32
+
33
+ margin-bottom: 5px;
34
+ }
35
+
36
+ dd {
37
+ width: 65%;
38
+ display: inline-block;
39
+ margin-bottom: 5px;
40
+ }
41
+
42
+ dl dt {
43
+ font-weight: bold;
44
+ }
45
+
46
+
47
+ #footer {
48
+ font-size: .75em;
49
+ text-align: center;
50
+ margin-top: 20px;
51
+ }
52
+
53
+ <% capnotify.components.each do |component| %>
54
+ <%= component.custom_css %>
55
+
56
+ <% end %>
57
+
58
+ </style>
59
+ </head>
60
+ <body>
61
+
62
+ <h1>
63
+ <%= capnotify.appname %> deployment completed!
64
+ </h1>
65
+
66
+ <% capnotify.components.each do |component| %>
67
+ <div class="<%= component.css_class %>">
68
+ <h2><%= component.header %></h2>
69
+
70
+ <%= component.render_content(:html) %>
71
+ </div>
72
+ <% end %>
73
+
74
+ <div id="footer">
75
+ Email generated by <a href="https://github.com/spikegrobstein/capnotify">Capnotify</a>
76
+ </div>
77
+ </body>
78
+ </html>
@@ -0,0 +1,8 @@
1
+ <%= capnotify.appname %> deployment completed!
2
+
3
+ <% capnotify.components.each do |component| %>
4
+ --- <%= @header %> ---
5
+ <%= component.render_content(:txt) %>
6
+ <% end %>
7
+
8
+ Email generated by Capnotify <https://github.com/spikegrobstein/capnotify>
@@ -0,0 +1,3 @@
1
+ module Capnotify
2
+ VERSION = "0.1.0pre"
3
+ end
data/lib/capnotify.rb ADDED
@@ -0,0 +1,132 @@
1
+ require "capnotify/version"
2
+ require 'capnotify/component'
3
+ require 'capnotify/plugin'
4
+ require 'capnotify/plugin/overview'
5
+ require 'capnotify/plugin/details'
6
+
7
+ module Capnotify
8
+ def self.load_into(config)
9
+ config.load do
10
+ Capistrano.plugin :capnotify, ::Capnotify::Plugin
11
+
12
+ def _cset(name, *args, &block)
13
+ unless exists?(name)
14
+ set(name, *args, &block)
15
+ end
16
+ end
17
+
18
+
19
+ # some configuration
20
+ _cset :capnotify_deployment_notification_html_template_path, capnotify.built_in_template_for('default_notification.html.erb')
21
+ _cset :capnotify_deployment_notification_text_template_path, capnotify.built_in_template_for('default_notification.txt.erb')
22
+
23
+ # get the name of the user deploying
24
+ # if using git, this will read that from your git config
25
+ # otherwise will use the currently logged-in user's name
26
+ _cset(:deployer_username) do
27
+ if exists?(:scm) && fetch(:scm).to_sym == :git
28
+ `git config user.name`.chomp
29
+ else
30
+ `whoami`.chomp
31
+ end
32
+ end
33
+
34
+ # built-in values:
35
+ set :capnotify_component_list, []
36
+
37
+ # override this to change the default behavior for capnotify.appname
38
+ _cset(:capnotify_appname) do
39
+ [ fetch(:application, nil), fetch(:stage, nil) ].compact.join(" ")
40
+ end
41
+
42
+ # default messages:
43
+ # (these can be overridden)
44
+
45
+ # short message for the start of running migrations
46
+ _cset(:capnotify_migrate_start_msg) do
47
+ "#{ capnotify.appname } migration starting."
48
+ end
49
+
50
+ # short message for the completion of running migrations
51
+ _cset(:capnotify_migrate_complete_msg) do
52
+ "#{ capnotify.appname } migration completed."
53
+ end
54
+
55
+ # short message for the start of a deployment
56
+ _cset(:capnotify_deploy_start_msg) do
57
+ "#{ capnotify.appname } deployment completed."
58
+ end
59
+
60
+ # short message for the completion of a deployment
61
+ _cset(:capnotify_deploy_complete_msg) do
62
+ "#{ capnotify.appname } deployment completed."
63
+ end
64
+
65
+ # short message for putting up a maintenance page
66
+ _cset(:capnotify_maintenance_up_msg) do
67
+ "#{ capnotify.appname } maintenance page is now up."
68
+ end
69
+
70
+ # short message for taking down a maintenance page
71
+ _cset(:capnotify_maintenance_down_msg) do
72
+ "#{ capnotify.appname } maintenance page has been taken down."
73
+ end
74
+
75
+ # full email message to notify of deployment (html)
76
+ _cset(:capnotify_deployment_notification_html) do
77
+ capnotify.build_template( fetch(:capnotify_deployment_notification_html_template_path) )
78
+ end
79
+
80
+ # full email message to notify of deployment (plain text)
81
+ _cset(:capnotify_deployment_notification_text) do
82
+ data = capnotify.build_template( fetch(:capnotify_deployment_notification_text_template_path) )
83
+
84
+ # clean up the text output (remove leading spaces and more than 2 newlines in a row
85
+ data.gsub(/^ +/, '').gsub(/\n{3,}/, "\n\n")
86
+ end
87
+
88
+ # before update_code, fetch the current revision
89
+ # this is needed to ensure that no matter when capnotify fetches the commit logs,
90
+ # it will have the correct starting point.
91
+ before 'deploy:update_code' do
92
+ set :capnotify_previous_revision, fetch(:current_revision, nil) # the revision that's currently deployed at this moment
93
+ end
94
+
95
+ # configure the callbacks
96
+
97
+ on(:load) do
98
+ # deploy start/complete
99
+ unless fetch(:capnotify_disable_deploy_hooks, false)
100
+ before('deploy') { trigger :deploy_start }
101
+ after('deploy') { trigger :deploy_complete }
102
+ end
103
+
104
+ # migration start/complete
105
+ unless fetch(:capnotify_disable_migrate_hooks, false)
106
+ before('deploy:migrate') { trigger :migrate_start }
107
+ after('deploy:migrate') { trigger :migrate_complete }
108
+ end
109
+
110
+ # maintenance start/complete
111
+ unless fetch(:capnotify_disable_maintenance_hooks, false)
112
+ after('deploy:web:disable') { trigger :maintenance_page_up }
113
+ after('deploy:web:enable') { trigger :maintenance_page_down }
114
+ end
115
+
116
+ unless fetch(:capnotify_disable_default_components, false)
117
+ capnotify.load_plugin :capnotify_overview, Capnotify::Plugin::Overview
118
+ capnotify.load_plugin :capnotify_details, Capnotify::Plugin::Details
119
+ end
120
+
121
+ capnotify.print_splash
122
+ end
123
+
124
+ end
125
+ end
126
+
127
+ end
128
+
129
+ if Capistrano::Configuration.instance
130
+ Capnotify.load_into(Capistrano::Configuration.instance)
131
+ end
132
+
@@ -0,0 +1,100 @@
1
+ require 'spec_helper'
2
+
3
+ describe Capnotify::Component do
4
+ let(:component) { Capnotify::Component.new(:test_component) }
5
+
6
+ context "initialization" do
7
+
8
+ it "should set the name to the symbol" do
9
+ Capnotify::Component.new('new_component').name.should == :new_component
10
+ end
11
+
12
+ it "should set the header if specified" do
13
+ Capnotify::Component.new('asdf', :header => 'spike').header.should == 'spike'
14
+ end
15
+
16
+ it "should set the css_class if specified" do
17
+ Capnotify::Component.new('asdf', :css_class => 'great-component').css_class.should == 'great-component'
18
+ end
19
+
20
+ it "should set the css_class to the default 'section' if not specified" do
21
+ Capnotify::Component.new('asdf').css_class.should == 'section'
22
+ end
23
+
24
+ it "should allow building with a block" do
25
+ c = Capnotify::Component.new(:test_component) do |c|
26
+ c.header = 'My Header'
27
+
28
+ c.content = {}
29
+ c.content['this is'] = 'a test'
30
+ end
31
+
32
+ c.builder.should_not be_nil
33
+ c.header.should be_nil
34
+
35
+ c.build!(nil)
36
+
37
+ c.header.should == 'My Header'
38
+ c.builder.should be_nil
39
+ end
40
+ end
41
+
42
+ context "#render_content" do
43
+ let(:sample_content) { "This is sample content that just works." }
44
+
45
+ before do
46
+ component.content = sample_content
47
+ end
48
+
49
+ context "when using an existing renderer" do
50
+
51
+ it "should render data" do
52
+ component.renderers[:txt].should_not be_nil
53
+ component.render_content(:txt).should match(sample_content)
54
+ end
55
+
56
+ end
57
+
58
+ context "when a template is missing" do
59
+
60
+ it "should raise an error" do
61
+ component.render_for :txt => 'does_not_exist.erb'
62
+ expect { component.render_content(:txt) }.to raise_error
63
+ end
64
+
65
+ end
66
+
67
+ context "when a template is not defined" do
68
+
69
+ it "should return an empty string" do
70
+ component.renderers[:foo].should be_nil
71
+ component.render_content(:foo).should == ''
72
+ end
73
+
74
+ end
75
+
76
+ end
77
+
78
+ context "#template_path_for" do
79
+
80
+ it "should raise a TemplateUndefined error if the renderer is not defined" do
81
+ lambda { component.template_path_for(:foo) }.should raise_error(Capnotify::Component::TemplateUndefined)
82
+ end
83
+
84
+ end
85
+
86
+ context "#render_for" do
87
+
88
+ it "should add new renderers" do
89
+ expect { component.render_for :other => 'asdf.erb', :more => 'more.erb' }.to change { component.renderers.keys.count }.by(2)
90
+ end
91
+
92
+ it "should override existing renderers" do
93
+ expect { component.render_for :html => 'new_html.erb' }.to change { component.renderers.keys.count }.by(0)
94
+
95
+ component.renderers[:html].should == 'new_html.erb'
96
+ end
97
+
98
+ end
99
+
100
+ end