capistrano-campout 0.0.1

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 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: []