capistrano-campout 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ .rvmrc
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in capistrano-campout.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,33 @@
1
+ Copyright (c) 2012 Jason Adam Young
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23
+
24
+ Additional Copyrights
25
+ =====================
26
+
27
+ This code also contains code from the following people:
28
+
29
+ rails_config : Copyright (c) 2010 Jacques Crocker
30
+ deep_merge : Copyright (c) 2008 Steve Midgley, now mainted by Daniel DeLeo
31
+ eycap : Copyright (c) 2008-2011 Engine Yard
32
+ capfire : Copyright (c) 2010 Piet Jaspers 10to1
33
+ capistrano-mountaintop : Copyright (c) 2010 Joshua Nichols.
data/README.md ADDED
@@ -0,0 +1,98 @@
1
+ # Capistrano::Campout
2
+
3
+ Capistrano::Campout is a gem extension to capistrano to post/speak messages
4
+ and paste logs from a capistrano deployment to a campfire room. Settings are
5
+ configurable in a "config/campout.yml" and/or "config/campout.local.yml"
6
+ file. ERB templates are supported for messages and can use all of the
7
+ capistrano settings.
8
+
9
+ Capistrano::Campout will a speak messages at pre-deployment and
10
+ post-deployment success or failure. Event sounds are also supported.
11
+
12
+ ## Design goals
13
+
14
+ Capistrano::Campout is insipred by and borrows concepts (and in some cases code)
15
+ from two projects: [capistrano-mountaintop](https://github.com/technicalpickles/capistrano-mountaintop) and [capfire](https://github.com/pjaspers/capfire).
16
+
17
+ I created my own instead of forking either for the following reasons
18
+
19
+ * I wanted to use Tinder (Capfire uses broach)
20
+ * I wanted the necessary functionality built in instead of using additional gems (mountaintop depends on capistrano-campfire and capistrano-log_with_awesome)
21
+ * I wanted configuration-file based settings that I can separate into shared (checked into git) and local (git ignored) files
22
+ * I wanted room to expand for my team's needs for utilities for git and github inspection
23
+ * Because I could.
24
+
25
+ ## Installation
26
+
27
+ Add this line to your application's Gemfile:
28
+
29
+ gem 'capistrano-campout'
30
+
31
+ And then execute:
32
+
33
+ $ bundle
34
+
35
+ Or install it yourself as:
36
+
37
+ $ gem install capistrano-campout
38
+
39
+ ## Usage
40
+
41
+ Add a:
42
+
43
+ require "capistrano-campout"
44
+
45
+ to your capistrano deploy.rb and create a config/campout.yml and/or a config/campout.local.yml with your campfire settings (required)
46
+
47
+ campfire:
48
+ domain: 'your_campfire_domain'
49
+ room: room_id_to_post_to
50
+ token: your_campfire_api_token
51
+
52
+ "config/campout.yml" is meant to be a pre-project file and can contain global settings for everyone. Don't put the api token in a campout.yml file that's part of a public git repository
53
+
54
+ "config/campout.local.yml" is meant as a local/private configuration file - I'd recommend adding the file to the .gitignore
55
+
56
+ ## Additional Settings
57
+
58
+ I'll write those up one day. Until that day, use the source, Luke.
59
+
60
+ ## Known Limitations
61
+
62
+ ### Deploy:Cleanup task
63
+
64
+ Capistrano::Campout requires [Tinder](https://github.com/collectiveidea/tinder) - which requires [ActiveSupport](https://github.com/rails/rails/tree/master/activesupport)
65
+
66
+ Apparently, Capistrano's "deploy:cleanup" task breaks due to conflicts between Capistrano and ActiveSupport
67
+
68
+ See:
69
+
70
+ * [Issue #169](https://github.com/capistrano/capistrano/issues/169)
71
+ * [Issue #170](https://github.com/capistrano/capistrano/issues/170)
72
+ * [Pull Request #175](https://github.com/capistrano/capistrano/pull/175)
73
+
74
+ Until that's fixed, you'll have to comment out any "deploy:cleanup" invocations
75
+
76
+ ### Error checking
77
+
78
+ There isn't much. And there's no tests. The latter might be a feature.
79
+
80
+ ## Contributing
81
+
82
+ 1. Fork it
83
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
84
+ 3. Commit your changes (`git commit -am 'Added some feature'`)
85
+ 4. Push to the branch (`git push origin my-new-feature`)
86
+ 5. Create new Pull Request
87
+
88
+ ## Sources
89
+
90
+ Capistrano::Campout includes code and ideas from the following projects:
91
+
92
+ * [rails_config](https://github.com/railsjedi/rails_config) Copyright © 2010 Jacques Crocker
93
+ * [deep_merge](https://github.com/danielsdeleo/deep_merge) Copyright © 2008 Steve Midgley, now mainted by Daniel DeLeo
94
+ * [eycap](https://github.com/engineyard/eycap) Copyright © 2008-2011 Engine Yard
95
+ * [capfire](https://github.com/pjaspers/capfire) Copyright © 2010 Piet Jaspers 10to1
96
+ * [capistrano-mountaintop](https://github.com/technicalpickles/capistrano-mountaintop) Copyright © 2010 Joshua Nichols.
97
+
98
+
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
@@ -0,0 +1,25 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/capistrano-campout/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Jason Adam Young"]
6
+ gem.email = ["jay@outfielding.net"]
7
+ gem.description = <<-EOF
8
+ Capistrano::Campout is a gem extension to capistrano to post/speak messages and paste logs from a capistrano deployment
9
+ to a campfire room. Settings are configurable using ERB in a "config/campout.yml" or "config/campout.local.yml" file.
10
+ Capistrano::Campout will a speak a pre-deployment message, and a post-deployment success or failure message. Event sounds
11
+ are also supported.
12
+ EOF
13
+ gem.summary = %q{Post messages and paste logs from a capistrano deployment to a campfire room}
14
+ gem.homepage = %q{https://github.com/jasonadamyoung/capistrano-campout}
15
+ gem.license = 'MIT'
16
+ gem.files = `git ls-files`.split($\)
17
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
18
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
19
+ gem.name = "capistrano-campout"
20
+ gem.require_paths = ["lib"]
21
+ gem.version = Capistrano::Campout::VERSION
22
+ gem.add_dependency('capistrano', '>= 2.11')
23
+ gem.add_dependency('tinder', '>= 1.8')
24
+ gem.add_dependency('grit', '>= 2.4')
25
+ end
@@ -0,0 +1,50 @@
1
+ # === COPYRIGHT:
2
+ # Copyright (c) 2012 Jason Adam Young
3
+ # === LICENSE:
4
+ # see LICENSE file
5
+
6
+ Capistrano::Configuration.instance(:must_exist).load do
7
+ # Don't bother users who have capfire installed but don't have a ~/.campfire file
8
+
9
+ if Capfire.config_file_exists?
10
+ if Capfire.valid_config?
11
+ before "deploy:update_code", "capfire:check_for_push"
12
+ after "deploy:update_code", "capfire:post_to_campfire"
13
+ else
14
+ logger.info "Not all required keys found in your .campfire file. Please regenerate."
15
+ end
16
+ else
17
+ logger.info "Couldn't find a .campfire in your home directory."
18
+ end
19
+
20
+
21
+
22
+ desc <<-DESC
23
+ This will post to the campfire room as specified in your ~/.campfire. \
24
+ The message posted will contain a link to Github's excellent compare view, \
25
+ the commiters name, the project name and the arguments supplied to cap.
26
+ DESC
27
+ task :post_to_campfire do
28
+ begin
29
+ source_repo_url = repository
30
+ deployed_version = previous_revision[0,7] rescue "000000"
31
+ local_version = `git rev-parse HEAD`[0,7]
32
+
33
+ compare_url = Capfire.github_compare_url source_repo_url, deployed_version, local_version
34
+ message = Capfire.deploy_message(ARGV.join(' '), compare_url, application)
35
+ message = `cowsay "#{message}"` if Capfire.cowsay?
36
+
37
+ if dry_run
38
+ logger.info "Capfire would have posted:\n#{message}"
39
+ else
40
+ Capfire.speak message
41
+ end
42
+ logger.info "Posting to Campfire"
43
+ rescue => e
44
+ # Making sure we don't make capistrano fail.
45
+ # Cause nothing sucks donkeyballs like not being able to deploy
46
+ logger.important e.message
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,202 @@
1
+ # === COPYRIGHT:
2
+ # Copyright (c) 2012 Jason Adam Young
3
+ # === LICENSE:
4
+ # see LICENSE file
5
+ module Capistrano
6
+ module Campout
7
+ class Core
8
+ attr_accessor :settings, :campfire, :room
9
+
10
+ def initialize
11
+ @settings = Options.new
12
+ @settings.load!
13
+ end
14
+
15
+ def local_deployer
16
+ ENV['USER']
17
+ end
18
+
19
+ def git_deployer
20
+ if(gitutils)
21
+ gitutils.user_name
22
+ else
23
+ nil
24
+ end
25
+ end
26
+
27
+ def gitutils
28
+ @gitutils ||= GitUtils.new('.')
29
+ end
30
+
31
+ def campout_deployer
32
+ if(!(deployer = git_deployer))
33
+ deployer = local_deployer
34
+ end
35
+ deployer
36
+ end
37
+
38
+ def speak(msg)
39
+ room.speak(msg)
40
+ end
41
+
42
+ def play(sound)
43
+ room.play(sound)
44
+ end
45
+
46
+ def paste(text)
47
+ room.paste(text)
48
+ end
49
+
50
+ def campfire
51
+ @campfire ||= connect_to_campfire
52
+ end
53
+
54
+ def connect_to_campfire
55
+ options = {token: settings.campfire.token}
56
+ if(!settings.campfire.ssl.nil?)
57
+ options[:ssl] = settings.campfire.ssl
58
+ end
59
+
60
+ if(!settings.campfire.ssl_verify.nil?)
61
+ options[:ssl_verify] = settings.campfire.ssl_verify
62
+ end
63
+ Tinder::Campfire.new(settings.campfire.domain,options)
64
+ end
65
+
66
+ def pre_announce(options = {})
67
+ self.speak(ERB.new(settings.pre_deploy.message).result(options[:binding]))
68
+ if(!settings.suppress_sounds and settings.pre_deploy.play)
69
+ self.play(settings.pre_deploy.play)
70
+ end
71
+ @pre_announce_time = Time.now
72
+ end
73
+
74
+ def post_announce_success(options = {})
75
+ message = ERB.new(settings.post_deploy_success.message).result(options[:binding])
76
+ if(!settings.suppress_deploy_time and @pre_announce_time)
77
+ message += " (#{time_period_to_s(Time.now - @pre_announce_time)})"
78
+ end
79
+
80
+ if(!settings.suppress_github_compare and gitutils)
81
+ github_compare = true
82
+ repository = options[:repository] || github_compare = false
83
+ gitutils.repository = repository
84
+ previous = options[:previous_revision] || github_compare = false
85
+ latest = options[:latest_revision] || github_compare = false
86
+ else
87
+ github_compare = false
88
+ end
89
+
90
+ if(gitutils)
91
+ gitutils.user_name
92
+ else
93
+ nil
94
+ end
95
+
96
+ if(github_compare and gitutils.repository_is_github?)
97
+ message += " #{gitutils.github_compare_url(previous,latest)}"
98
+ end
99
+
100
+ self.speak(message)
101
+ if(!settings.suppress_sounds and settings.post_deploy_success.play)
102
+ self.play(settings.post_deploy_success.play)
103
+ end
104
+
105
+ if(!settings.suppress_deploy_log_paste)
106
+ logger = Capistrano::CampoutLogger
107
+ log_output = File.open(logger.log_file_path).read
108
+ self.paste(log_output)
109
+ end
110
+
111
+ @pre_announce_time = nil
112
+ end
113
+
114
+ def post_announce_failure(options = {})
115
+ message = ERB.new(settings.post_deploy_failure.message).result(options[:binding])
116
+ self.speak(message)
117
+ if(!settings.suppress_sounds and settings.post_deploy_failure.play)
118
+ self.play(settings.post_deploy_failure.play)
119
+ end
120
+
121
+ if(!settings.suppress_deploy_log_paste)
122
+ logger = Capistrano::CampoutLogger
123
+ log_output = File.open(logger.log_file_path).read
124
+ self.paste(log_output)
125
+ end
126
+ @pre_announce_time = nil
127
+ end
128
+
129
+
130
+ def room
131
+ @room ||= campfire.find_room_by_id(settings.campfire.room)
132
+ end
133
+
134
+ def will_do(options = {})
135
+ puts "Before Deployment:"
136
+ puts "Message: #{ERB.new(settings.pre_deploy.message).result(options[:binding])}"
137
+ if(!settings.suppress_sounds and settings.pre_deploy.play)
138
+ puts "Will play sound: #{settings.pre_deploy.play}"
139
+ else
140
+ puts "Will not play sound"
141
+ end
142
+
143
+ puts "\n"
144
+ puts "After Successful Deployment:"
145
+ puts "Message: #{ERB.new(settings.post_deploy_success.message).result(options[:binding])}"
146
+ if(!settings.suppress_sounds and settings.post_deploy_success.play)
147
+ puts "Will play sound: #{settings.post_deploy_success.play}"
148
+ else
149
+ puts "Will not play sound"
150
+ end
151
+ if(!settings.suppress_deploy_log_paste)
152
+ puts "Will paste deployment log"
153
+ else
154
+ puts "Will not paste deployment log"
155
+ end
156
+
157
+
158
+ puts "\n"
159
+ puts "After Failed Deployment:"
160
+ puts "Message: #{ERB.new(settings.post_deploy_failure.message).result(options[:binding])}"
161
+ if(!settings.suppress_sounds and settings.post_deploy_failure.play)
162
+ puts "Will play sound: #{settings.post_deploy_failure.play}"
163
+ else
164
+ puts "Will not play sound"
165
+ end
166
+ if(!settings.suppress_deploy_log_paste)
167
+ puts "Will paste deployment log"
168
+ else
169
+ puts "Will not paste deployment log"
170
+ end
171
+ end
172
+
173
+ # Takes a period of time in seconds and returns it in human-readable form (down to minutes)
174
+ # code from http://www.postal-code.com/binarycode/2007/04/04/english-friendly-timespan/
175
+ def time_period_to_s(time_period,abbreviated=false,defaultstring = '')
176
+ out_str = ''
177
+ if(time_period.blank?)
178
+ return defaultstring
179
+ end
180
+ interval_array = [ [:weeks, 604800], [:days, 86400], [:hours, 3600], [:minutes, 60], [:seconds, 1] ]
181
+ interval_array.each do |sub|
182
+ if time_period >= sub[1] then
183
+ time_val, time_period = time_period.divmod( sub[1] )
184
+ if(abbreviated)
185
+ name = sub[0].to_s.first
186
+ ( sub[0] != :seconds ? out_str += ", " : out_str += " " ) if out_str != ''
187
+ else
188
+ time_val == 1 ? name = sub[0].to_s.chop : name = sub[0].to_s
189
+ ( sub[0] != :seconds ? out_str += ", " : out_str += " and " ) if out_str != ''
190
+ end
191
+ out_str += time_val.to_s + " #{name}"
192
+ end
193
+ end
194
+ if(out_str.blank?)
195
+ return defaultstring
196
+ else
197
+ return out_str
198
+ end
199
+ end
200
+ end
201
+ end
202
+ end
@@ -0,0 +1,216 @@
1
+ # deep_merge was written by Steve Midgley, and is now maintained by Daniel DeLeo.
2
+ # The official home of deep_merge on the internet is now
3
+ # https://github.com/danielsdeleo/deep_merge
4
+ #
5
+ # Copyright (c) 2008 Steve Midgley, released under the MIT license
6
+
7
+ module DeepMerge
8
+
9
+ class InvalidParameter < StandardError; end
10
+
11
+ DEFAULT_FIELD_KNOCKOUT_PREFIX = '--'
12
+
13
+ # Deep Merge core documentation.
14
+ # deep_merge! method permits merging of arbitrary child elements. The two top level
15
+ # elements must be hashes. These hashes can contain unlimited (to stack limit) levels
16
+ # of child elements. These child elements to not have to be of the same types.
17
+ # Where child elements are of the same type, deep_merge will attempt to merge them together.
18
+ # Where child elements are not of the same type, deep_merge will skip or optionally overwrite
19
+ # the destination element with the contents of the source element at that level.
20
+ # So if you have two hashes like this:
21
+ # source = {:x => [1,2,3], :y => 2}
22
+ # dest = {:x => [4,5,'6'], :y => [7,8,9]}
23
+ # dest.deep_merge!(source)
24
+ # Results: {:x => [1,2,3,4,5,'6'], :y => 2}
25
+ # By default, "deep_merge!" will overwrite any unmergeables and merge everything else.
26
+ # To avoid this, use "deep_merge" (no bang/exclamation mark)
27
+ #
28
+ # Options:
29
+ # Options are specified in the last parameter passed, which should be in hash format:
30
+ # hash.deep_merge!({:x => [1,2]}, {:knockout_prefix => '--'})
31
+ # :preserve_unmergeables DEFAULT: false
32
+ # Set to true to skip any unmergeable elements from source
33
+ # :knockout_prefix DEFAULT: nil
34
+ # Set to string value to signify prefix which deletes elements from existing element
35
+ # :sort_merged_arrays DEFAULT: false
36
+ # Set to true to sort all arrays that are merged together
37
+ # :unpack_arrays DEFAULT: nil
38
+ # Set to string value to run "Array::join" then "String::split" against all arrays
39
+ # :merge_hash_arrays DEFAULT: false
40
+ # Set to true to merge hashes within arrays
41
+ # :merge_debug DEFAULT: false
42
+ # Set to true to get console output of merge process for debugging
43
+ #
44
+ # Selected Options Details:
45
+ # :knockout_prefix => The purpose of this is to provide a way to remove elements
46
+ # from existing Hash by specifying them in a special way in incoming hash
47
+ # source = {:x => ['--1', '2']}
48
+ # dest = {:x => ['1', '3']}
49
+ # dest.ko_deep_merge!(source)
50
+ # Results: {:x => ['2','3']}
51
+ # Additionally, if the knockout_prefix is passed alone as a string, it will cause
52
+ # the entire element to be removed:
53
+ # source = {:x => '--'}
54
+ # dest = {:x => [1,2,3]}
55
+ # dest.ko_deep_merge!(source)
56
+ # Results: {:x => ""}
57
+ # :unpack_arrays => The purpose of this is to permit compound elements to be passed
58
+ # in as strings and to be converted into discrete array elements
59
+ # irsource = {:x => ['1,2,3', '4']}
60
+ # dest = {:x => ['5','6','7,8']}
61
+ # dest.deep_merge!(source, {:unpack_arrays => ','})
62
+ # Results: {:x => ['1','2','3','4','5','6','7','8'}
63
+ # Why: If receiving data from an HTML form, this makes it easy for a checkbox
64
+ # to pass multiple values from within a single HTML element
65
+ #
66
+ # :merge_hash_arrays => merge hashes within arrays
67
+ # source = {:x => [{:y => 1}]}
68
+ # dest = {:x => [{:z => 2}]}
69
+ # dest.deep_merge!(source, {:merge_hash_arrays => true})
70
+ # Results: {:x => [{:y => 1, :z => 2}]}
71
+ #
72
+ # There are many tests for this library - and you can learn more about the features
73
+ # and usages of deep_merge! by just browsing the test examples
74
+ def self.deep_merge!(source, dest, options = {})
75
+ # turn on this line for stdout debugging text
76
+ merge_debug = options[:merge_debug] || false
77
+ overwrite_unmergeable = !options[:preserve_unmergeables]
78
+ knockout_prefix = options[:knockout_prefix] || nil
79
+ raise InvalidParameter, "knockout_prefix cannot be an empty string in deep_merge!" if knockout_prefix == ""
80
+ raise InvalidParameter, "overwrite_unmergeable must be true if knockout_prefix is specified in deep_merge!" if knockout_prefix && !overwrite_unmergeable
81
+ # if present: we will split and join arrays on this char before merging
82
+ array_split_char = options[:unpack_arrays] || false
83
+ # request that we sort together any arrays when they are merged
84
+ sort_merged_arrays = options[:sort_merged_arrays] || false
85
+ # request that arrays of hashes are merged together
86
+ merge_hash_arrays = options[:merge_hash_arrays] || false
87
+ di = options[:debug_indent] || ''
88
+ # do nothing if source is nil
89
+ return dest if source.nil?
90
+ # if dest doesn't exist, then simply copy source to it
91
+ if !(dest) && overwrite_unmergeable
92
+ dest = source; return dest
93
+ end
94
+
95
+ puts "#{di}Source class: #{source.class.inspect} :: Dest class: #{dest.class.inspect}" if merge_debug
96
+ if source.kind_of?(Hash)
97
+ puts "#{di}Hashes: #{source.inspect} :: #{dest.inspect}" if merge_debug
98
+ source.each do |src_key, src_value|
99
+ if dest.kind_of?(Hash)
100
+ puts "#{di} looping: #{src_key.inspect} => #{src_value.inspect} :: #{dest.inspect}" if merge_debug
101
+ if dest[src_key]
102
+ puts "#{di} ==>merging: #{src_key.inspect} => #{src_value.inspect} :: #{dest[src_key].inspect}" if merge_debug
103
+ dest[src_key] = deep_merge!(src_value, dest[src_key], options.merge(:debug_indent => di + ' '))
104
+ else # dest[src_key] doesn't exist so we want to create and overwrite it (but we do this via deep_merge!)
105
+ puts "#{di} ==>merging over: #{src_key.inspect} => #{src_value.inspect}" if merge_debug
106
+ # note: we rescue here b/c some classes respond to "dup" but don't implement it (Numeric, TrueClass, FalseClass, NilClass among maybe others)
107
+ begin
108
+ src_dup = src_value.dup # we dup src_value if possible because we're going to merge into it (since dest is empty)
109
+ rescue TypeError
110
+ src_dup = src_value
111
+ end
112
+ dest[src_key] = deep_merge!(src_value, src_dup, options.merge(:debug_indent => di + ' '))
113
+ end
114
+ else # dest isn't a hash, so we overwrite it completely (if permitted)
115
+ if overwrite_unmergeable
116
+ puts "#{di} overwriting dest: #{src_key.inspect} => #{src_value.inspect} -over-> #{dest.inspect}" if merge_debug
117
+ dest = overwrite_unmergeables(source, dest, options)
118
+ end
119
+ end
120
+ end
121
+ elsif source.kind_of?(Array)
122
+ puts "#{di}Arrays: #{source.inspect} :: #{dest.inspect}" if merge_debug
123
+ # if we are instructed, join/split any source arrays before processing
124
+ if array_split_char
125
+ puts "#{di} split/join on source: #{source.inspect}" if merge_debug
126
+ source = source.join(array_split_char).split(array_split_char)
127
+ if dest.kind_of?(Array)
128
+ dest = dest.join(array_split_char).split(array_split_char)
129
+ end
130
+ end
131
+ # if there's a naked knockout_prefix in source, that means we are to truncate dest
132
+ if source.index(knockout_prefix)
133
+ dest = clear_or_nil(dest); source.delete(knockout_prefix)
134
+ end
135
+ if dest.kind_of?(Array)
136
+ if knockout_prefix
137
+ print "#{di} knocking out: " if merge_debug
138
+ # remove knockout prefix items from both source and dest
139
+ source.delete_if do |ko_item|
140
+ retval = false
141
+ item = ko_item.respond_to?(:gsub) ? ko_item.gsub(%r{^#{knockout_prefix}}, "") : ko_item
142
+ if item != ko_item
143
+ print "#{ko_item} - " if merge_debug
144
+ dest.delete(item)
145
+ dest.delete(ko_item)
146
+ retval = true
147
+ end
148
+ retval
149
+ end
150
+ puts if merge_debug
151
+ end
152
+ puts "#{di} merging arrays: #{source.inspect} :: #{dest.inspect}" if merge_debug
153
+ source_all_hashes = source.all? { |i| i.kind_of?(Hash) }
154
+ dest_all_hashes = dest.all? { |i| i.kind_of?(Hash) }
155
+ if merge_hash_arrays && source_all_hashes && dest_all_hashes
156
+ # merge hashes in lists
157
+ list = []
158
+ dest.each_index do |i|
159
+ list[i] = deep_merge!(source[i] || {}, dest[i],
160
+ options.merge(:debug_indent => di + ' '))
161
+ end
162
+ list += source[dest.count..-1] if source.count > dest.count
163
+ dest = list
164
+ else
165
+ dest = dest | source
166
+ end
167
+ dest.sort! if sort_merged_arrays
168
+ elsif overwrite_unmergeable
169
+ puts "#{di} overwriting dest: #{source.inspect} -over-> #{dest.inspect}" if merge_debug
170
+ dest = overwrite_unmergeables(source, dest, options)
171
+ end
172
+ else # src_hash is not an array or hash, so we'll have to overwrite dest
173
+ puts "#{di}Others: #{source.inspect} :: #{dest.inspect}" if merge_debug
174
+ dest = overwrite_unmergeables(source, dest, options)
175
+ end
176
+ puts "#{di}Returning #{dest.inspect}" if merge_debug
177
+ dest
178
+ end # deep_merge!
179
+
180
+ # allows deep_merge! to uniformly handle overwriting of unmergeable entities
181
+ def self.overwrite_unmergeables(source, dest, options)
182
+ merge_debug = options[:merge_debug] || false
183
+ overwrite_unmergeable = !options[:preserve_unmergeables]
184
+ knockout_prefix = options[:knockout_prefix] || false
185
+ di = options[:debug_indent] || ''
186
+ if knockout_prefix && overwrite_unmergeable
187
+ if source.kind_of?(String) # remove knockout string from source before overwriting dest
188
+ src_tmp = source.gsub(%r{^#{knockout_prefix}},"")
189
+ elsif source.kind_of?(Array) # remove all knockout elements before overwriting dest
190
+ src_tmp = source.delete_if {|ko_item| ko_item.kind_of?(String) && ko_item.match(%r{^#{knockout_prefix}}) }
191
+ else
192
+ src_tmp = source
193
+ end
194
+ if src_tmp == source # if we didn't find a knockout_prefix then we just overwrite dest
195
+ puts "#{di}#{src_tmp.inspect} -over-> #{dest.inspect}" if merge_debug
196
+ dest = src_tmp
197
+ else # if we do find a knockout_prefix, then we just delete dest
198
+ puts "#{di}\"\" -over-> #{dest.inspect}" if merge_debug
199
+ dest = ""
200
+ end
201
+ elsif overwrite_unmergeable
202
+ dest = source
203
+ end
204
+ dest
205
+ end
206
+
207
+ def self.clear_or_nil(obj)
208
+ if obj.respond_to?(:clear)
209
+ obj.clear
210
+ else
211
+ obj = nil
212
+ end
213
+ obj
214
+ end
215
+
216
+ end # module DeepMerge
@@ -0,0 +1,6 @@
1
+ pre_deploy:
2
+ message: "<%= campout_deployer %> is starting to deploy <%= application %> to <%= stage %>"
3
+ post_deploy_success:
4
+ message: "<%= campout_deployer %> deployed <%= application %> to <%= stage %>"
5
+ post_deploy_failure:
6
+ message: "The deploy of <%= application %> to <%= stage %> by <%= campout_deployer %> has failed."
@@ -0,0 +1,74 @@
1
+ # === COPYRIGHT:
2
+ # Copyright (c) 2012 Jason Adam Young
3
+ # === LICENSE:
4
+ # see LICENSE file
5
+ module Capistrano
6
+ module Campout
7
+ class GitUtils
8
+
9
+ def initialize(path)
10
+ @path = path
11
+ if(localrepo)
12
+ return self
13
+ else
14
+ return nil
15
+ end
16
+ end
17
+
18
+ def localrepo
19
+ if(@localrepo.nil?)
20
+ begin
21
+ @localrepo = Grit::Repo.new(@path)
22
+ rescue Grit::InvalidGitRepositoryError
23
+ end
24
+ end
25
+ @localrepo
26
+ end
27
+
28
+ def user_name
29
+ if(localrepo)
30
+ git_config = Grit::Config.new(localrepo)
31
+ git_config.fetch('user.name')
32
+ else
33
+ nil
34
+ end
35
+ end
36
+
37
+ def repository=(repository)
38
+ @repository = repository
39
+ end
40
+
41
+ def github_url_for_repository
42
+ if(!@mogrified_repository)
43
+ if(@repository =~ /git@github.com/)
44
+ @mogrified_repository = @repository.dup
45
+ @mogrified_repository.gsub!(/git@/, 'http://')
46
+ @mogrified_repository.gsub!(/\.com:/,'.com/')
47
+ @mogrified_repository.gsub!(/\.git/, '')
48
+ end
49
+ end
50
+ @mogrified_repository
51
+ end
52
+
53
+ def repository_is_github?
54
+ return (!github_url_for_repository.nil?)
55
+ end
56
+
57
+ def github_compare_url(previous_revision, latest_revision)
58
+ if(repository_is_github?)
59
+ if(previous_revision != latest_revision)
60
+ "#{github_url_for_repository}/compare/#{previous_revision}...#{latest_revision}"
61
+ else
62
+ "#{github_url_for_repository}/commit/#{latest_revision}"
63
+ end
64
+ else
65
+ nil
66
+ end
67
+ end
68
+
69
+ end
70
+ end
71
+ end
72
+
73
+
74
+
@@ -0,0 +1,127 @@
1
+ #
2
+ # Original source from: https://github.com/engineyard/eycap/blob/master/lib/eycap/lib/ey_logger.rb
3
+ # Copyright (c) 2008-2011 Engine Yard, released under the MIT License
4
+ #
5
+ require 'tmpdir'
6
+ require 'fileutils'
7
+ module Capistrano
8
+
9
+ class Logger
10
+
11
+ def campout_log(level, message, line_prefix = nil)
12
+ CampoutLogger.log(level, message, line_prefix) if CampoutLogger.setup?
13
+ log_without_campout_logging(level, message, line_prefix)
14
+ end
15
+
16
+ unless method_defined?(:log_without_campout_logging)
17
+ alias_method :log_without_campout_logging, :log
18
+ alias_method :log, :campout_log
19
+ end
20
+
21
+ def close
22
+ device.close if @needs_close
23
+ CampoutLogger.close if CampoutLogger.setup?
24
+ end
25
+ end
26
+
27
+ class CampoutLogger
28
+
29
+ # Sets up the CampoutLogger to begin capturing capistrano's logging. You should pass the capistrano configuration
30
+ def self.setup(configuration, options = {})
31
+ @_configuration = configuration
32
+ @_log_path = options[:deploy_log_path] || Dir.tmpdir
33
+ @_log_path << "/" unless @_log_path =~ /\/$/
34
+ FileUtils.mkdir_p(@_log_path)
35
+ @_setup = true
36
+ @_success = true
37
+ end
38
+
39
+ def self.log(level, message, line_prefix=nil)
40
+ return nil unless setup?
41
+ @release_name = @_configuration[:release_name] if @release_name.nil?
42
+ @_log_file_path = @_log_path + @release_name + ".log" unless @_log_file_path
43
+ @_deploy_log_file = File.open(@_log_file_path, "w") if @_deploy_log_file.nil?
44
+
45
+ indent = "%*s" % [Logger::MAX_LEVEL, "*" * (Logger::MAX_LEVEL - level)]
46
+ message.each_line do |line|
47
+ if line_prefix
48
+ @_deploy_log_file << "#{indent} [#{line_prefix}] #{line.strip}\n"
49
+ else
50
+ @_deploy_log_file << "#{indent} #{line.strip}\n"
51
+ end
52
+ end
53
+ end
54
+
55
+ def self.post_process
56
+ unless ::Interrupt === $!
57
+ puts "\n\nPlease wait while the log file is processed\n"
58
+ # Should dump the stack trace of an exception if there is one
59
+ error = $!
60
+ unless error.nil?
61
+ @_deploy_log_file << error.message << "\n"
62
+ @_deploy_log_file << error.backtrace.join("\n")
63
+ @_success = false
64
+ end
65
+ self.close
66
+
67
+ hooks = [:any]
68
+ hooks << (self.successful? ? :success : :failure)
69
+ puts "Executing Post Processing Hooks"
70
+ hooks.each do |h|
71
+ @_post_process_hooks[h].each do |key|
72
+ @_configuration.parent.find_and_execute_task(key)
73
+ end
74
+ end
75
+ puts "Finished Post Processing Hooks"
76
+ end
77
+ end
78
+
79
+ # Adds a post processing hook.
80
+ #
81
+ # Provide a task name to execute. These tasks are executed after capistrano has actually run its course.
82
+ #
83
+ # Takes a key to control when the hook is executed.'
84
+ # :any - always executed
85
+ # :success - only execute on success
86
+ # :failure - only execute on failure
87
+ #
88
+ # ==== Example
89
+ # Capistrano::CampoutLogger.post_process_hook( "campout:post_log", :any)
90
+ #
91
+ def self.post_process_hook(task, key = :any)
92
+ @_post_process_hooks ||= Hash.new{|h,k| h[k] = []}
93
+ @_post_process_hooks[key] << task
94
+ end
95
+
96
+ def self.setup?
97
+ !!@_setup
98
+ end
99
+
100
+ def self.deploy_type
101
+ @_deploy_type
102
+ end
103
+
104
+ def self.successful?
105
+ !!@_success
106
+ end
107
+
108
+ def self.failure?
109
+ !@_success
110
+ end
111
+
112
+ def self.log_file_path
113
+ @_log_file_path
114
+ end
115
+
116
+ def self.remote_log_file_name
117
+ @_log_file_name ||= "deploy-#{@_configuration[:release_name]}-#{self.successful? ? "success" : "failure"}.log"
118
+ end
119
+
120
+ def self.close
121
+ @_deploy_log_file.flush unless @_deploy_log_file.nil?
122
+ @_deploy_log_file.close unless @_deploy_log_file.nil?
123
+ @_setup = false
124
+ end
125
+
126
+ end
127
+ end
@@ -0,0 +1,149 @@
1
+ #
2
+ # The core of options.rb comes from rails_config, https://github.com/railsjedi/rails_config
3
+ #
4
+ # Copyright (c) 2010 Jacques Crocker, released under the MIT license
5
+ #
6
+ #
7
+
8
+ require 'ostruct'
9
+ require 'yaml'
10
+ require 'erb'
11
+
12
+ module Capistrano
13
+ module Campout
14
+ module Sources
15
+ class YAMLSource
16
+
17
+ attr_accessor :path
18
+
19
+ def initialize(path)
20
+ @path = path
21
+ end
22
+
23
+ # returns a config hash from the YML file
24
+ def load
25
+ if @path and File.exists?(@path.to_s)
26
+ result = YAML.load(IO.read(@path.to_s))
27
+ end
28
+ result || {}
29
+ end
30
+
31
+ end
32
+ end
33
+
34
+ class Options < OpenStruct
35
+ attr_accessor :defaults
36
+
37
+ def valid?
38
+ # requires the following settings
39
+ # campfire:
40
+ # domain:
41
+ # token:
42
+ # room:
43
+
44
+ return false if(self.campfire.nil?)
45
+ return false if(self.campfire.domain.nil?)
46
+ return false if(self.campfire.token.nil?)
47
+ return false if(self.campfire.room.nil?)
48
+ return true
49
+ end
50
+
51
+ def files=(*files)
52
+ if(!files.empty?)
53
+ @files = [files].flatten.compact.uniq
54
+ end
55
+ end
56
+
57
+ def files
58
+ if(@files.nil? or @files.empty?)
59
+ @files = ["#{File.join(File.dirname(__FILE__), "defaults.yml").to_s}","./config/campout.yml","./config/campout.local.yml"]
60
+ end
61
+ @files
62
+ end
63
+
64
+ def empty?
65
+ marshal_dump.empty?
66
+ end
67
+
68
+ def load_from_files(*files)
69
+ self.files=files
70
+ self.load!
71
+ end
72
+
73
+ def reload_from_files(*files)
74
+ self.files=files
75
+ self.reload!
76
+ end
77
+
78
+ def reset_sources!
79
+ self.files.each do |file|
80
+ source = (Sources::YAMLSource.new(file)) if file.is_a?(String)
81
+ @config_sources ||= []
82
+ @config_sources << source
83
+ end
84
+ end
85
+
86
+ # look through all our sources and rebuild the configuration
87
+ def reload!
88
+ self.reset_sources!
89
+ conf = {}
90
+ @config_sources.each do |source|
91
+ source_conf = source.load
92
+
93
+ if conf.empty?
94
+ conf = source_conf
95
+ else
96
+ DeepMerge.deep_merge!(source_conf, conf, :preserve_unmergeables => false)
97
+ end
98
+ end
99
+
100
+ # swap out the contents of the OStruct with a hash (need to recursively convert)
101
+ marshal_load(__convert(conf).marshal_dump)
102
+
103
+ return self
104
+ end
105
+
106
+ alias :load! :reload!
107
+
108
+ def to_hash
109
+ result = {}
110
+ marshal_dump.each do |k, v|
111
+ result[k] = v.instance_of?(RailsConfig::Options) ? v.to_hash : v
112
+ end
113
+ result
114
+ end
115
+
116
+ def to_json(*args)
117
+ require "json" unless defined?(JSON)
118
+ to_hash.to_json(*args)
119
+ end
120
+
121
+ def merge!(hash)
122
+ current = to_hash
123
+ DeepMerge.deep_merge!(current, hash.dup)
124
+ marshal_load(__convert(hash).marshal_dump)
125
+ self
126
+ end
127
+
128
+ protected
129
+
130
+ # Recursively converts Hashes to Options (including Hashes inside Arrays)
131
+ def __convert(h) #:nodoc:
132
+ s = self.class.new
133
+
134
+ h.each do |k, v|
135
+ s.new_ostruct_member(k)
136
+
137
+ if v.is_a?(Hash)
138
+ v = v["type"] == "hash" ? v["contents"] : __convert(v)
139
+ elsif v.is_a?(Array)
140
+ v = v.collect { |e| e.instance_of?(Hash) ? __convert(e) : e }
141
+ end
142
+
143
+ s.send("#{k}=".to_sym, v)
144
+ end
145
+ s
146
+ end
147
+ end
148
+ end
149
+ end
@@ -0,0 +1,49 @@
1
+ # === COPYRIGHT:
2
+ # Copyright (c) 2012 Jason Adam Young
3
+ # === LICENSE:
4
+ # see LICENSE file
5
+
6
+ #
7
+ # Post process hooks inspired by: https://github.com/engineyard/eycap/blob/master/lib/eycap/lib/ey_logger_hooks.rb
8
+ # Copyright (c) 2008-2011 Engine Yard, released under the MIT License
9
+ #
10
+
11
+ Capistrano::Configuration.instance(:must_exist).load do
12
+ namespace :campout do
13
+
14
+ ## no descriptions for the following tasks - meant to be hooked by capistrano
15
+ task :pre_announce do
16
+ campout_core.pre_announce(binding: binding)
17
+ end
18
+
19
+ task :post_announce_success do
20
+ campout_core.post_announce_success(binding: binding,
21
+ repository: repository,
22
+ previous_revision: previous_revision,
23
+ latest_revision: latest_revision)
24
+ end
25
+
26
+ task :post_announce_failure do
27
+ campout_core.post_announce_failure(binding: binding)
28
+ end
29
+
30
+ task :copy_log, :except => { :no_release => true} do
31
+ logger = Capistrano::CampoutLogger
32
+ run "mkdir -p #{shared_path}/deploy_logs"
33
+ put File.open(logger.log_file_path).read, "#{shared_path}/deploy_logs/#{logger.remote_log_file_name}"
34
+ end
35
+
36
+ desc "Display campfire messages and actions based on current configuration"
37
+ task :will_do do
38
+ campout_core.will_do(binding: binding)
39
+ end
40
+
41
+ task :settings do
42
+ y campout_core.settings
43
+ end
44
+ end
45
+ end
46
+
47
+ Capistrano::CampoutLogger.post_process_hook("campout:post_announce_success",:success)
48
+ Capistrano::CampoutLogger.post_process_hook("campout:post_announce_failure",:failure)
49
+ Capistrano::CampoutLogger.post_process_hook("campout:copy_log",:any)
@@ -0,0 +1,20 @@
1
+ # === COPYRIGHT:
2
+ # Copyright (c) 2012 Jason Adam Young
3
+ # === LICENSE:
4
+ # see LICENSE file
5
+
6
+ #
7
+ # Original source from: https://github.com/engineyard/eycap/blob/master/lib/eycap/recipes/deploy.rb
8
+ # Copyright (c) 2008-2011 Engine Yard, released under the MIT License
9
+ #
10
+
11
+ Capistrano::Configuration.instance(:must_exist).load do
12
+ namespace :deploy do
13
+ # This is here to hook into the logger for deploy tasks
14
+ before("deploy") do
15
+ Capistrano::CampoutLogger.setup(self)
16
+ at_exit{ Capistrano::CampoutLogger.post_process if Capistrano::CampoutLogger.setup? }
17
+ end
18
+ end
19
+ end
20
+
@@ -0,0 +1,5 @@
1
+ module Capistrano
2
+ module Campout
3
+ VERSION = "0.0.1"
4
+ end
5
+ end
@@ -0,0 +1,39 @@
1
+ # === COPYRIGHT:
2
+ # Copyright (c) 2012 Jason Adam Young
3
+ # === LICENSE:
4
+ # see LICENSE file
5
+ require 'capistrano'
6
+ require 'tinder'
7
+ require 'grit'
8
+
9
+ require 'capistrano-campout/logger'
10
+ require 'capistrano-campout/version'
11
+ require 'capistrano-campout/deep_merge' unless defined?(DeepMerge)
12
+ require 'capistrano-campout/options'
13
+ require 'capistrano-campout/git_utils'
14
+ require 'capistrano-campout/core'
15
+
16
+ module Capistrano
17
+ module Campout
18
+ def self.extended(configuration)
19
+ configuration.load do
20
+ set :campout_core, Capistrano::Campout::Core.new
21
+ set :campout_deployer, campout_core.campout_deployer
22
+ end
23
+ end
24
+ end
25
+ end
26
+
27
+ if Capistrano::Configuration.instance
28
+ Capistrano::Configuration.instance.extend(Capistrano::Campout)
29
+ end
30
+
31
+ Capistrano::Configuration.instance(:must_exist).load do
32
+ if(campout_core.settings.valid?)
33
+ # load the recipes
34
+ Dir.glob(File.join(File.dirname(__FILE__), '/capistrano-campout/recipes/*.rb')).sort.each { |f| load f }
35
+ before "deploy", "campout:pre_announce"
36
+ else
37
+ logger.info "The campout configuration is not valid. Make sure that the campfire settings are specified in the campout configuration file(s)"
38
+ end
39
+ end
metadata ADDED
@@ -0,0 +1,100 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: capistrano-campout
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Jason Adam Young
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-04-07 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: capistrano
16
+ requirement: &70114418631680 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '2.11'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *70114418631680
25
+ - !ruby/object:Gem::Dependency
26
+ name: tinder
27
+ requirement: &70114418615660 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: '1.8'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: *70114418615660
36
+ - !ruby/object:Gem::Dependency
37
+ name: grit
38
+ requirement: &70114418614880 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ! '>='
42
+ - !ruby/object:Gem::Version
43
+ version: '2.4'
44
+ type: :runtime
45
+ prerelease: false
46
+ version_requirements: *70114418614880
47
+ description: ! " Capistrano::Campout is a gem extension to capistrano to post/speak
48
+ messages and paste logs from a capistrano deployment \n to a campfire room. Settings
49
+ are configurable using ERB in a \"config/campout.yml\" or \"config/campout.local.yml\"
50
+ file. \n Capistrano::Campout will a speak a pre-deployment message, and a post-deployment
51
+ success or failure message. Event sounds\n are also supported.\n"
52
+ email:
53
+ - jay@outfielding.net
54
+ executables: []
55
+ extensions: []
56
+ extra_rdoc_files: []
57
+ files:
58
+ - .gitignore
59
+ - Gemfile
60
+ - LICENSE
61
+ - README.md
62
+ - Rakefile
63
+ - capistrano-campout.gemspec
64
+ - lib/capistrano-campout.rb
65
+ - lib/capistrano-campout/cap_tasks.rb
66
+ - lib/capistrano-campout/core.rb
67
+ - lib/capistrano-campout/deep_merge.rb
68
+ - lib/capistrano-campout/defaults.yml
69
+ - lib/capistrano-campout/git_utils.rb
70
+ - lib/capistrano-campout/logger.rb
71
+ - lib/capistrano-campout/options.rb
72
+ - lib/capistrano-campout/recipes/campout.rb
73
+ - lib/capistrano-campout/recipes/deploy.rb
74
+ - lib/capistrano-campout/version.rb
75
+ homepage: https://github.com/jasonadamyoung/capistrano-campout
76
+ licenses:
77
+ - MIT
78
+ post_install_message:
79
+ rdoc_options: []
80
+ require_paths:
81
+ - lib
82
+ required_ruby_version: !ruby/object:Gem::Requirement
83
+ none: false
84
+ requirements:
85
+ - - ! '>='
86
+ - !ruby/object:Gem::Version
87
+ version: '0'
88
+ required_rubygems_version: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ! '>='
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ requirements: []
95
+ rubyforge_project:
96
+ rubygems_version: 1.8.17
97
+ signing_key:
98
+ specification_version: 3
99
+ summary: Post messages and paste logs from a capistrano deployment to a campfire room
100
+ test_files: []