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.
- data/.gitignore +17 -0
- data/.rspec +1 -0
- data/.travis.yml +6 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +376 -0
- data/Rakefile +1 -0
- data/capnotify.gemspec +28 -0
- data/lib/capnotify/component.rb +88 -0
- data/lib/capnotify/plugin/details.rb +26 -0
- data/lib/capnotify/plugin/overview.rb +23 -0
- data/lib/capnotify/plugin.rb +121 -0
- data/lib/capnotify/templates/_component.html.erb +20 -0
- data/lib/capnotify/templates/_component.txt.erb +11 -0
- data/lib/capnotify/templates/default_notification.html.erb +78 -0
- data/lib/capnotify/templates/default_notification.txt.erb +8 -0
- data/lib/capnotify/version.rb +3 -0
- data/lib/capnotify.rb +132 -0
- data/spec/capnotify/component_spec.rb +100 -0
- data/spec/capnotify/plugin_spec.rb +256 -0
- data/spec/capnotify_spec.rb +196 -0
- data/spec/spec_helper.rb +15 -0
- metadata +170 -0
@@ -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,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>
|
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
|