capnotify 0.1.0pre

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