wesabot 1.0.1

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