wesabot 1.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.
Files changed (55) hide show
  1. data/.gitignore +5 -0
  2. data/Gemfile +3 -0
  3. data/LICENSE +20 -0
  4. data/README.md +182 -0
  5. data/Rakefile +13 -0
  6. data/bin/wesabot +111 -0
  7. data/config/wesabot.yml.sample +4 -0
  8. data/lib/campfire/bot.rb +53 -0
  9. data/lib/campfire/configuration.rb +65 -0
  10. data/lib/campfire/message.rb +102 -0
  11. data/lib/campfire/polling_bot/plugin.rb +162 -0
  12. data/lib/campfire/polling_bot/plugins/airbrake/.gitignore +1 -0
  13. data/lib/campfire/polling_bot/plugins/airbrake/airbrake/api.rb +48 -0
  14. data/lib/campfire/polling_bot/plugins/airbrake/airbrake/error.rb +51 -0
  15. data/lib/campfire/polling_bot/plugins/airbrake/airbrake_plugin.rb +63 -0
  16. data/lib/campfire/polling_bot/plugins/airbrake/airbrake_plugin.yml.sample +3 -0
  17. data/lib/campfire/polling_bot/plugins/bookmark/bookmark.rb +15 -0
  18. data/lib/campfire/polling_bot/plugins/bookmark/bookmark_plugin.rb +89 -0
  19. data/lib/campfire/polling_bot/plugins/debug/debug_plugin.rb +23 -0
  20. data/lib/campfire/polling_bot/plugins/deploy/deploy_plugin.rb +155 -0
  21. data/lib/campfire/polling_bot/plugins/deploy/deploy_plugin.yml.sample +9 -0
  22. data/lib/campfire/polling_bot/plugins/deploy/spec/deploy_plugin_spec.rb +153 -0
  23. data/lib/campfire/polling_bot/plugins/greeting/greeting_plugin.rb +146 -0
  24. data/lib/campfire/polling_bot/plugins/greeting/greeting_setting.rb +19 -0
  25. data/lib/campfire/polling_bot/plugins/greeting/spec/greeting_plugin_spec.rb +51 -0
  26. data/lib/campfire/polling_bot/plugins/greeting/spec/greeting_setting_spec.rb +61 -0
  27. data/lib/campfire/polling_bot/plugins/help/help_plugin.rb +53 -0
  28. data/lib/campfire/polling_bot/plugins/history/history_plugin.rb +34 -0
  29. data/lib/campfire/polling_bot/plugins/image_search/displayed_image.rb +8 -0
  30. data/lib/campfire/polling_bot/plugins/image_search/image_search_plugin.rb +106 -0
  31. data/lib/campfire/polling_bot/plugins/interjector/interjector_plugin.rb +58 -0
  32. data/lib/campfire/polling_bot/plugins/kibitz/kibitz_plugin.rb +90 -0
  33. data/lib/campfire/polling_bot/plugins/kibitz/spec/kibitz_plugin_spec.rb +56 -0
  34. data/lib/campfire/polling_bot/plugins/plugin.db +0 -0
  35. data/lib/campfire/polling_bot/plugins/reload/reload_plugin.rb +24 -0
  36. data/lib/campfire/polling_bot/plugins/remind_me/remind_me_plugin.rb +149 -0
  37. data/lib/campfire/polling_bot/plugins/remind_me/reminder.rb +8 -0
  38. data/lib/campfire/polling_bot/plugins/shared/message.rb +37 -0
  39. data/lib/campfire/polling_bot/plugins/shared/user.rb +32 -0
  40. data/lib/campfire/polling_bot/plugins/sms/sms_plugin.rb +88 -0
  41. data/lib/campfire/polling_bot/plugins/sms/sms_setting.rb +7 -0
  42. data/lib/campfire/polling_bot/plugins/time/time_plugin.rb +17 -0
  43. data/lib/campfire/polling_bot/plugins/tweet/tweet.rb +52 -0
  44. data/lib/campfire/polling_bot/plugins/tweet/tweet_plugin.rb +117 -0
  45. data/lib/campfire/polling_bot/plugins/tweet/tweet_plugin.yml.sample +4 -0
  46. data/lib/campfire/polling_bot/plugins/twitter_search/twitter_search_plugin.rb +58 -0
  47. data/lib/campfire/polling_bot.rb +125 -0
  48. data/lib/campfire/sample_plugin.rb +56 -0
  49. data/lib/campfire/version.rb +3 -0
  50. data/lib/wesabot.rb +3 -0
  51. data/spec/.gitignore +1 -0
  52. data/spec/polling_bot_spec.rb +23 -0
  53. data/spec/spec_helper.rb +190 -0
  54. data/wesabot.gemspec +55 -0
  55. metadata +336 -0
@@ -0,0 +1,162 @@
1
+ # Campfire AbstractPollingBot Plugin base class
2
+ #
3
+ # To create a plugin, extend from this class, and just drop it into the plugins directory.
4
+ # See sample_plugin.rb for more information.
5
+ #
6
+ require 'dm-core'
7
+ require 'dm-migrations'
8
+
9
+ module Campfire
10
+ class PollingBot
11
+ class Plugin
12
+ attr_accessor :config
13
+
14
+ def initialize
15
+ # load the config file if we have one
16
+ name = self.to_s.gsub(/([[:upper:]]+)([[:upper:]][[:lower:]])/,'\1_\2').
17
+ gsub(/([[:lower:]\d])([[:upper:]])/,'\1_\2').
18
+ tr("-", "_").
19
+ downcase
20
+ filepath = File.join(self.class.directory, "#{name}.yml")
21
+ if File.exists?(filepath)
22
+ self.config = YAML.load_file(filepath)
23
+ else
24
+ self.config = {}
25
+ end
26
+ end
27
+
28
+ # keep track of subclasses
29
+ def self.inherited(klass)
30
+ # save the plugin's directory
31
+ filepath = caller[0].split(':')[0]
32
+ klass.directory = File.dirname(caller[0].split(':')[0])
33
+ super if defined? super
34
+ ensure
35
+ ( @subclasses ||= [] ).push(klass).uniq!
36
+ end
37
+
38
+ def self.subclasses
39
+ @subclasses ||= []
40
+ @subclasses.inject( [] ) do |list, subclass|
41
+ list.push(subclass, *subclass.subclasses)
42
+ end
43
+ end
44
+
45
+ def self.directory=(dir)
46
+ @directory = dir
47
+ end
48
+
49
+ def self.directory
50
+ @directory
51
+ end
52
+
53
+ # bot accessor
54
+ def self.bot
55
+ @@bot
56
+ end
57
+ def self.bot=(bot)
58
+ @@bot = bot
59
+ end
60
+ attr_writer :bot
61
+ def bot
62
+ @bot || self.class.bot
63
+ end
64
+
65
+ def logger
66
+ bot.logger
67
+ end
68
+
69
+ HALT = 1 # returned by a command when command processing should halt (continues by default)
70
+
71
+ def self.load_all(bot)
72
+ self.bot = bot
73
+
74
+ load_plugin_classes
75
+
76
+ # set up the database now that the plugins are loaded
77
+ setup_database(bot.config.datauri)
78
+
79
+ plugin_classes = self.subclasses.sort {|a,b| b.priority <=> a.priority }
80
+ # initialize plugins
81
+ plugins = plugin_classes.map { |p_class| p_class.new }
82
+ return plugins
83
+ end
84
+
85
+ def self.load_plugin_classes
86
+ # add each plugin dir to the load path
87
+ Dir.glob(File.dirname(__FILE__) + "/plugins/*").each {|dir| $LOAD_PATH << dir }
88
+ # load core first
89
+ paths = Dir.glob(File.dirname(__FILE__) + "/plugins/shared/*.rb")
90
+ # load all models & plugins
91
+ paths += Dir.glob(File.dirname(__FILE__) + "/plugins/*/*.rb")
92
+ paths.each do |path|
93
+ begin
94
+ path.match(/(.*?)\.rb$/) && (require $1)
95
+ rescue Exception => e
96
+ $stderr.puts "Unable to load #{path}: #{e.class}: #{e.message}\n\t#{e.backtrace.join("\n\t")}"
97
+ end
98
+ end
99
+ end
100
+
101
+ # set up the plugin database
102
+ def self.setup_database(datauri)
103
+ DataMapper.setup(:default, datauri)
104
+ DataMapper.auto_upgrade!
105
+ # not related to setting up the database, but for lack of a better place....
106
+ DataMapper::Model.raise_on_save_failure = true
107
+ end
108
+
109
+ # method to set or get the priority. Higher value == higher priority. Default is 0
110
+ # command subclasses set their priority like so:
111
+ # class FooPlugin << Campfire::PollingBot::Plugin
112
+ # priority 10
113
+ # ...
114
+ def self.priority(value = nil)
115
+ if value
116
+ @priority = value
117
+ end
118
+ return @priority || 0
119
+ end
120
+
121
+ # convenience method to get the priority of a plugin instance
122
+ def priority
123
+ self.class.priority
124
+ end
125
+
126
+ # called from Plugin objects to indicate what kinds of messages they accept
127
+ # if the :addressed_to_me flag is true, it will only accept messages addressed
128
+ # to the bot (e.g. "Wes, ____" or "______, Wes")
129
+ # Examples:
130
+ # accepts :text_message, :addressed_to_me => true
131
+ # accepts :enter_message
132
+ # accepts :all
133
+ def self.accepts(message_type, params = {})
134
+ @accepts ||= {}
135
+ if message_type == :all
136
+ @accepts[:all] = params[:addressed_to_me] ? :addressed_to_me : :for_anyone
137
+ else
138
+ klass = Campfire.const_get(message_type.to_s.gsub(/(?:^|_)(\S)/) {$1.upcase})
139
+ @accepts[klass] = params[:addressed_to_me] ? :addressed_to_me : :for_anyone
140
+ end
141
+ end
142
+
143
+ # returns true if the plugin accepts the given message type
144
+ def self.accepts?(message)
145
+ if @accepts[:all]
146
+ @accepts[:all] == :addressed_to_me ? bot.addressed_to_me?(message) : true
147
+ elsif @accepts[message.class]
148
+ @accepts[message.class] == :addressed_to_me ? bot.addressed_to_me?(message) : true
149
+ end
150
+ end
151
+
152
+ # convenience method to call accepts on a plugin instance
153
+ def accepts?(message)
154
+ self.class.accepts?(message)
155
+ end
156
+
157
+ def to_s
158
+ self.class.to_s
159
+ end
160
+ end
161
+ end
162
+ end
@@ -0,0 +1 @@
1
+ airbrake_plugin.yml
@@ -0,0 +1,48 @@
1
+ require 'nokogiri'
2
+ require 'rest-client'
3
+
4
+ module Airbrake
5
+ class API
6
+ def initialize(domain, auth_token)
7
+ @host = "#{domain}.airbrakeapp.com"
8
+ @auth_token = auth_token
9
+ end
10
+
11
+ def resolve_error(error_id, resolved=true)
12
+ api_put("/errors/#{error_id}", {}, :group => {:resolved => resolved})
13
+ end
14
+
15
+ def errors(show_resolved=false)
16
+ doc = Nokogiri::XML(api_get("/errors.xml", :show_resolved => show_resolved))
17
+ doc.xpath("/groups/group").map {|node| Airbrake::Error.from_xml(node) }
18
+ end
19
+
20
+ def error_url(error)
21
+ error_id = error.is_a?(Airbrake::Error) ? error.error_id : error
22
+ "https://#{@host}/errors/#{error_id}"
23
+ end
24
+
25
+ private
26
+
27
+ def api_uri(endpoint, query = {})
28
+ query = {:auth_token => @auth_token}.update(query)
29
+ uri = URI::HTTPS.build(:host => @host, :path => endpoint, :query => to_query(query))
30
+ return uri.to_s
31
+ end
32
+
33
+ # convert a hash of param to a query string
34
+ def to_query(params)
35
+ params.map{|k,v| "#{CGI::escape(k.to_s)}=#{CGI::escape(v.to_s)}"}.join('&')
36
+ end
37
+
38
+ def api_get(endpoint, query = {})
39
+ RestClient.get(api_uri(endpoint, query))
40
+ end
41
+
42
+ def api_put(endpoint, query = {}, params = {})
43
+ return RestClient.put(api_uri(endpoint, query), params)
44
+ rescue RestClient::Exception => e
45
+ return e.response
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,51 @@
1
+ module Airbrake
2
+ class Error
3
+ include DataMapper::Resource
4
+ property :id, Serial
5
+ property :error_id, Integer, :required => true
6
+
7
+ attr_accessor :field
8
+
9
+ # Given a block of XML returned from the Airbrake API, return an
10
+ # Airbrake::Error object
11
+ def self.from_xml(xml)
12
+ field = {}
13
+ xml.xpath('./*').each do |node|
14
+ content = node.content
15
+ # convert to proper ruby types
16
+ type = node.attributes['type'] && node.attributes['type'].value
17
+ case type
18
+ when "boolean"
19
+ content = content == "true"
20
+ when "datetime"
21
+ content = Time.parse(content)
22
+ when "integer"
23
+ content = content.to_i
24
+ end
25
+ key = node.name.tr('-','_')
26
+ field[key.to_sym] = content
27
+ end
28
+ error = new()
29
+ error.field = field
30
+ error.error_id = field[:id]
31
+ return error
32
+ end
33
+
34
+ def summary
35
+ "#{@field[:error_message]} at #{@field[:file]}:#{@field[:line_number]}"
36
+ end
37
+
38
+ def [](key)
39
+ key = key.to_s.tr('-','_').to_sym
40
+ @field[key]
41
+ end
42
+
43
+ def eql?(other)
44
+ self.error_id.eql?(other.error_id)
45
+ end
46
+
47
+ def hash
48
+ self.error_id.hash
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,63 @@
1
+ require 'airbrake/api'
2
+ require 'airbrake/error'
3
+
4
+ class AirbrakePlugin < Campfire::PollingBot::Plugin
5
+ accepts :text_message, :addressed_to_me => true
6
+ priority 10
7
+
8
+ def initialize
9
+ super
10
+ @api = Airbrake::API.new(config['domain'], config['auth_token'])
11
+ end
12
+
13
+ def process(message)
14
+ case message.command
15
+ when /((?:un(?:-)?)?resolve) error (?:#|number)?\s*(\d+)/
16
+ action, error_num = $1, $2
17
+ res = @api.resolve_error(error_num, action == "resolve")
18
+ case res.code
19
+ when 200
20
+ bot.say("Ok, #{action}d error ##{error_num}")
21
+ when 404
22
+ bot.say("Hmm. Airbrake couldn't find error ##{error_num}")
23
+ else
24
+ bot.say("Huh. Airbrake gave me this response:")
25
+ bot.paste(res.to_s)
26
+ end
27
+ return HALT
28
+ end
29
+ end
30
+
31
+ def heartbeat
32
+ # check every <check_interval> seconds (heartbeat is called every 3 sec)
33
+ num_heartbeats = config['check_interval'] / Campfire::PollingBot::HEARTBEAT_INTERVAL
34
+ @heartbeat_counter ||= 0
35
+ @heartbeat_counter += 1
36
+ return unless (@heartbeat_counter % num_heartbeats) == 1
37
+ handle_errors
38
+ end
39
+
40
+ def handle_errors
41
+ # fetch errors we know about, announce new ones, and remove resolved ones
42
+ unresolved_errors = @api.errors
43
+ known_errors = Airbrake::Error.all
44
+ new_errors = unresolved_errors - known_errors
45
+ resolved_errors = known_errors - unresolved_errors
46
+ resolved_errors.each {|e| e.destroy }
47
+ new_errors.each {|e| e.save }
48
+ announce(new_errors) if new_errors.any?
49
+ end
50
+
51
+ def announce(errors)
52
+ msg = "Got #{errors.length} new error#{errors.length > 1 ? 's' : ''} from Airbrake" +
53
+ (errors.length > 5 ? ". Here are the first 5:" : ":")
54
+ bot.say(msg)
55
+ errors.first(5).each { |e| bot.say("#{e.summary} (#{@api.error_url(e)})") }
56
+ end
57
+
58
+ # return array of available commands and descriptions
59
+ def help
60
+ [["resolve <error number>", "mark an error as resolved"],
61
+ ["unresolve <error number>", "mark an error as unresolved"]]
62
+ end
63
+ end
@@ -0,0 +1,3 @@
1
+ domain: your-airbrake-domain
2
+ auth_token: your-airbrake-auth-token
3
+ check_interval: 300 # seconds between API checks
@@ -0,0 +1,15 @@
1
+ # used by BookmarksPlugin
2
+ class Bookmark
3
+ include DataMapper::Resource
4
+ property :id, Serial
5
+ property :room, Integer, :required => true, :index => true
6
+ property :message_id, Integer, :required => true
7
+ property :person, String, :index => true
8
+ property :name, String, :index => true
9
+ property :timestamp, Time, :required => true, :index => true
10
+
11
+ # return link to bookmark
12
+ def link
13
+ "/room/#{self.room}/transcript/message/#{self.message_id}"
14
+ end
15
+ end
@@ -0,0 +1,89 @@
1
+ # Plugin to allow saving of transcript bookmarks
2
+ class BookmarkPlugin < Campfire::PollingBot::Plugin
3
+ accepts :text_message, :addressed_to_me => true
4
+
5
+ def process(message)
6
+ case message.command
7
+ when /^(?:add |create )?bookmark:?\s*(?:this as:?)?\s*("?)(.*?)\1$/i
8
+ save_bookmark(message, $2)
9
+ bot.say("Ok, saved bookmark: #{$2}")
10
+ return HALT
11
+ when /(?:list|show) (\S+) bookmarks/i, /(?:list|show) bookmarks for (\S+)/i
12
+ list_bookmarks(message.person, $1)
13
+ return HALT
14
+ when /(list|show) bookmarks$/
15
+ list_bookmarks(message.person, message.person)
16
+ return HALT
17
+ when /delete bookmark (?:#\s*)?(\d+)/
18
+ delete_bookmark(message.person, $1.to_i)
19
+ return HALT
20
+
21
+ end
22
+ end
23
+
24
+ # return array of available commands and descriptions
25
+ def help
26
+ [["bookmark: <name>", "bookmark the current location"]]
27
+ end
28
+
29
+ private
30
+
31
+ def save_bookmark(message, name)
32
+ Bookmark.create(:person => message.person,
33
+ :name => name,
34
+ :room => bot.room.id,
35
+ :message_id => message.message_id,
36
+ :timestamp => Time.now)
37
+ end
38
+
39
+ def list_bookmarks(current_person, request_person)
40
+ case request_person.downcase
41
+ when 'my', 'me'
42
+ request_person = current_person
43
+ else
44
+ request_person.gsub!(/'s$/i,'')
45
+ end
46
+ request_person.capitalize!
47
+
48
+ case request_person
49
+ when /everyone/i, /all/i
50
+ if (bookmarks = Bookmark.all(:order => [:name])).any?
51
+ bot.say("Here are all the bookmarks I have:")
52
+ bookmarks.each do |bookmark|
53
+ bot.say("#{bookmark.id} - #{bookmark.name} (#{bookmark_link(bookmark)}) by #{bookmark.person}")
54
+ end
55
+ end
56
+ return
57
+ else
58
+ bookmarks = Bookmark.all(:conditions => {:person => request_person}, :order => [:name])
59
+ if bookmarks.any?
60
+ if request_person == current_person
61
+ bot.say("Here are the bookmarks I have for you, #{current_person}:")
62
+ else
63
+ bot.say("Here are the bookmarks for #{request_person}:")
64
+ end
65
+ bookmarks.each do |bookmark|
66
+ bot.say("#{bookmark.id} - #{bookmark.name} (#{bookmark_link(bookmark)})")
67
+ end
68
+ return
69
+ end
70
+ end
71
+ bot.say("I couldn't find any bookmarks for #{request_person}")
72
+ end
73
+
74
+ def delete_bookmark(current_person, id)
75
+ bookmark = Bookmark.get(id)
76
+ if bookmark.person == current_person
77
+ bookmark.destroy
78
+ bot.say("Ok, I've deleted bookmark ##{id}.")
79
+ else
80
+ bot.say("Sorry, #{current_person}, but I couldn't find a bookmark with that id that belongs to you.")
81
+ end
82
+ end
83
+
84
+ # return link to bookmark
85
+ def bookmark_link(bookmark)
86
+ bot.base_uri + bookmark.link
87
+ end
88
+
89
+ end
@@ -0,0 +1,23 @@
1
+ # Toggle debug mode
2
+ class DebugPlugin < Campfire::PollingBot::Plugin
3
+ accepts :text_message, :addressed_to_me => true
4
+ priority 100
5
+
6
+ def process(message)
7
+ case message.command
8
+ when /(enable|disable) debug/i
9
+ if $1 == 'enable'
10
+ bot.debug = true
11
+ else
12
+ bot.debug = false
13
+ end
14
+ bot.say("ok, debugging is #{bot.debug ? 'enabled' : 'disabled'}")
15
+ return HALT
16
+ end
17
+ end
18
+
19
+ # return array of available commands and descriptions
20
+ def help
21
+ [['<enable|disable> debugging', "enable or disable debug mode"]]
22
+ end
23
+ end
@@ -0,0 +1,155 @@
1
+ require 'open-uri'
2
+
3
+ # Plugin to get a list of commits that are on deck to be deployed
4
+ class DeployPlugin < Campfire::PollingBot::Plugin
5
+ accepts :text_message, :addressed_to_me => true
6
+
7
+ def process(message)
8
+ case message.command
9
+ when /deploy\s([^\s\!]+)(?:(?: to)? (staging|nine|production))?( with migrations)?/
10
+ project, env, migrate = $1, $2, $3
11
+ name = env ? "#{project} #{env}" : project
12
+ env ||= "production"
13
+
14
+ if not projects.any?
15
+ bot.say("Sorry #{message.person}, I don't know about any projects. Please configure the deploy plugin.")
16
+ return HALT
17
+ end
18
+
19
+ project ||= default_project
20
+ if project.nil?
21
+ bot.say("Sorry #{message.person}, I don't have a default project. Here are the projects I do know about:")
22
+ bot.paste(projects.keys.sort.join("\n"))
23
+ return HALT
24
+ end
25
+ project.downcase!
26
+
27
+ info = project_info(project)
28
+ if info.nil?
29
+ bot.say("Sorry #{message.person}, I don't know anything about #{name}. Here are the projects I do know about:")
30
+ bot.paste(projects.keys.sort.join("\n"))
31
+ return HALT
32
+ end
33
+
34
+ bot.say("Okay, trying to deploy #{name}...")
35
+
36
+ begin
37
+ deploy = migrate ? "deploy:migrations" : "deploy"
38
+ git(project, "bundle exec cap #{env} #{deploy}")
39
+ rescue => e
40
+ bot.log_error(e)
41
+ bot.say("Sorry #{message.person}, I couldn't deploy #{name}.")
42
+ return HALT
43
+ end
44
+
45
+ bot.say("Done.")
46
+ return HALT
47
+
48
+ when /on deck(?: for ([^\s\?]+)( staging)?)?/
49
+ project, staging = $1, $2
50
+ project ||= default_project
51
+ name = staging ? "#{project} staging" : project
52
+
53
+ if not projects.any?
54
+ bot.say("Sorry #{message.person}, I don't know about any projects. Please configure the deploy plugin.")
55
+ return HALT
56
+ end
57
+
58
+ if project.nil?
59
+ bot.say("Sorry #{message.person}, I don't have a default project. Here are the projects I do know about:")
60
+ bot.paste(projects.keys.sort.join("\n"))
61
+ return HALT
62
+ end
63
+ project.downcase!
64
+
65
+ info = project_info(project)
66
+ if info.nil?
67
+ bot.say("Sorry #{message.person}, I don't know anything about #{name}. Here are the projects I do know about:")
68
+ bot.paste(projects.keys.sort.join("\n"))
69
+ return HALT
70
+ end
71
+
72
+ range = nil
73
+ begin
74
+ range = "#{deployed_revision(project, staging)}..HEAD"
75
+ shortlog = project_shortlog(project, range)
76
+ rescue => e
77
+ bot.log_error(e)
78
+ bot.say("Sorry #{message.person}, I couldn't get what's on deck for #{name}.")
79
+ return HALT
80
+ end
81
+
82
+ if shortlog.nil? || shortlog =~ /\A\s*\Z/
83
+ bot.say("There's nothing on deck for #{name} right now.")
84
+ return HALT
85
+ end
86
+
87
+ bot.say("Here's what's on deck for #{name}:")
88
+ bot.paste("$ git shortlog #{range}\n\n#{shortlog}")
89
+
90
+ return HALT
91
+ end
92
+ end
93
+
94
+ def help
95
+ help_lines = [
96
+ ["what's on deck for <project>?", "shortlog of changes not yet deployed to production"],
97
+ ["what's on deck for <project> staging?", "shortlog of changes not yet deployed to staging"],
98
+ ]
99
+ if default_project
100
+ help_lines << ["what's on deck?", "shortlog of changes not yet deployed to #{default_project}"]
101
+ end
102
+ return help_lines
103
+ end
104
+
105
+ def projects
106
+ (config && config['projects']) || {}
107
+ end
108
+
109
+ def default_project
110
+ (config && config['default_project']) ||
111
+ (projects.size == 1 ? projects.keys.first : nil)
112
+ end
113
+
114
+ private
115
+
116
+ def project_info(project)
117
+ projects[project]
118
+ end
119
+
120
+ def project_shortlog(project, treeish)
121
+ info = project_info(project)
122
+ return nil if info.nil?
123
+
124
+ return git(project, "git shortlog #{treeish}")
125
+ end
126
+
127
+ def deployed_revision(project, staging = false)
128
+ info = project_info(project)
129
+ return nil if info.nil?
130
+
131
+ host = staging ? info['staging'] : info['url']
132
+ return nil if host.nil?
133
+
134
+ return open("http://#{host}/REVISION").read.chomp
135
+ end
136
+
137
+ def repository_path(project)
138
+ File.expand_path File.join(config["repository_base_path"], "#{project}")
139
+ end
140
+
141
+ def git(project, cmd)
142
+ dir = repository_path(project)
143
+ out = Dir.chdir(dir) do
144
+ # don't want output from the pull
145
+ system("git pull")
146
+ `#{cmd}`
147
+ end
148
+
149
+ unless $?.exitstatus.zero?
150
+ raise "attempt to run `#{cmd}` in #{dir} failed with status #{$?.exitstatus}\n#{out}"
151
+ end
152
+
153
+ return out
154
+ end
155
+ end
@@ -0,0 +1,9 @@
1
+ # config file for DeployPlugin
2
+ # sites are expected to have a file at /REVISION
3
+ # containing the git sha of the deployed revision
4
+ repository_base_path: src
5
+ default_project: my_super_site
6
+ projects:
7
+ my_super_site:
8
+ url: www.example.com
9
+ staging: staging.example.com