cerberus 0.1.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGES CHANGED
@@ -1,5 +1,24 @@
1
1
  = Cerberus Changelog
2
2
 
3
+ == Version 0.2.0
4
+ Config file was changed since 0.1.1 and you need regenerate config files.
5
+ Or change it by hands: see doc/OPTIONS file to list of all avalable options.
6
+
7
+ Changing required by advanced publishing mechanism. Now it is possible to add several published to one project.
8
+
9
+ Also added 3 new publishers.
10
+ Jabber Publisher - all notification sended via Jabber server
11
+ IRC publisher - messages sent to IRC channel
12
+ RSS Publisher - result of build Cerberus writes to file in RSS format.
13
+ This feature would be useful for big open project when many users would like to see results of test-run.
14
+ If you publish results as RSS - then any user could subscribe to channel.
15
+
16
+
17
+ * Added automatic subversion cleanup to avoid repository locking after Cedrberus process termination
18
+ * Added Jabber publisher
19
+ * Added IRC publisher
20
+ * Added RSS publisher
21
+
3
22
  == Version 0.1.1
4
23
  Minor improvements
5
24
 
data/README CHANGED
@@ -1,4 +1,4 @@
1
- Cerberus is a Continuous Integration (CI) software. Cerberus could be periodically run from scheduler and check if application tests are broken. If it happens then Cerberus will send notification to developers. Cerberus perfectly works both on Windows and *nix platforms.
1
+ Cerberus is a Continuous Builder software. Cerberus could be periodically run from scheduler and check if application tests are broken. If it happens then Cerberus will send notification to developers. Cerberus perfectly works both on Windows and *nix platforms.
2
2
 
3
3
  For more CI theory read this document from Martin Fowler
4
4
  http://www.martinfowler.com/articles/continuousIntegration.html.
@@ -17,6 +17,16 @@ He guarded the gate to Hades (the Greek underworld) and ensured that the dead co
17
17
 
18
18
  So Cerberus will guard your tests and not allow your project to go to the world of dead.
19
19
 
20
+ There is several solutions already present, why do you need to use Cerberus?
21
+ Main advantages of Cerberus over other solutions:
22
+ = Cerberus could be installed on any machine not only where SVN repository located.
23
+ = Cerberus works not only for Rails projects, but for any other Ruby (or better to say for projects that use Rake)
24
+ = Cerberus multiplatform solution: it runs excellent both on *nix and Windows.
25
+ = Cerberus distributed via RubyGems, so it is very easy to install and very easy to update to the latest available versin
26
+ = Cerberus very easy start to use. Just type 'cerberus add PROJECT_URL|PROJECT_DIR'
27
+ = Cerberus is lightweigt solution: mots of the time ruby process even not run - Rake runs only in case if changes in project found
28
+
29
+
20
30
  To use Cerberus it is very easy. First install it. Easiest way to do it through RubyGems package manager.
21
31
 
22
32
  'gem install cerberus'
@@ -31,12 +41,13 @@ as second parameter you could pass URL to subversion repository or directory wit
31
41
 
32
42
  Go to ~./cerberus and edit config.yml file (only once after installing Cerberus). Enter your configuration options here like email server, password, user_name and other options. See ActiveMailer description - Cerberus uses it as notification layer. My config file looks like this
33
43
 
34
- mail:
35
- address: mail.somesever.com
36
- user_name: anatol
37
- password: anatol
38
- domain: somesever.com
39
- authentication: login
44
+ publisher:
45
+ mail:
46
+ address: mail.somesever.com
47
+ user_name: anatol
48
+ password: anatol
49
+ domain: somesever.com
50
+ authentication: login
40
51
 
41
52
  Also check ~/.cerberus/config/<APPLICATION_NAME>.yml and make sure that you have right options.
42
53
 
@@ -44,8 +55,11 @@ And then run Cerberus
44
55
 
45
56
  cerberus build APPLICATION_NAME #Run project
46
57
 
47
- It will check out latest sources and run tests for your application. If tests are broken - recipients will receive notifications.
58
+ or
59
+
60
+ cerberus buildall #Run all available projects
48
61
 
49
- But of course better run Cerberus automatically from Cron. Run Cerberus for project each 10 minutes would be ok.
50
62
 
51
- Well, thats all. If you have any questions, proposals - just let me know.
63
+ It will check out latest sources and run tests for your application. If tests are broken - recipients will receive notifications.
64
+
65
+ But of course better run Cerberus automatically from Cron. Run Cerberus for project each 10 minutes would be ok.
data/Rakefile CHANGED
@@ -28,7 +28,9 @@ end
28
28
 
29
29
  desc "Clean all generated files"
30
30
  task :clean => :clobber_package do
31
- rm_rf './test/__workdir'
31
+ rm_rf "#{File.dirname(__FILE__)}/test/__workdir"
32
+ rm_rf "#{File.dirname(__FILE__)}/coverage"
33
+ rm_rf "#{File.dirname(__FILE__)}/doc/site/output"
32
34
  end
33
35
 
34
36
 
@@ -49,11 +51,14 @@ GEM_SPEC = Gem::Specification.new do |s|
49
51
  Cerberus could be easily invoked from Cron (for Unix) or nnCron (for Windows) utilities.
50
52
  DESC
51
53
 
52
- s.add_dependency 'actionmailer', '>= 1.2.1'
54
+ s.add_dependency 'actionmailer', '>= 1.2.3'
53
55
  s.add_dependency 'rake', '>= 0.7.1'
56
+ s.add_dependency 'jabber4r', '>= 0.8.0'
57
+ s.add_dependency 'Ruby-IRC', '>= 1.0.3'
54
58
 
55
- s.files = Dir.glob("{bin,doc,lib,test}/**/*").delete_if { |item| item.include?('__workdir') }
59
+ s.files = Dir.glob("{bin,lib,test}/**/*").delete_if { |item| item.include?('__workdir') }
56
60
  s.files += %w(LICENSE README CHANGES Rakefile)
61
+ s.files += Dir.glob("doc/*").delete_if { |item| item.include?('__workdir') }
57
62
 
58
63
  s.bindir = "bin"
59
64
  s.executables = ["cerberus"]
@@ -90,12 +95,27 @@ end
90
95
 
91
96
  desc "Look for TODO and FIXME tags in the code"
92
97
  task :todo do
93
- Pathname.new(File.dirname(__FILE__)).egrep(/#.*(FIXME|TODO|TBD|DEPRECATED)/) do |match|
94
- puts match
98
+ FileList.new(File.dirname(__FILE__)+'/**/*.rb').egrep(/#.*(FIXME|TODO|TBD|DEPRECATED)/i)
99
+ end
100
+
101
+ task :reinstall => [:uninstall, :install]
102
+
103
+ task :site_webgen do
104
+ sh %{pushd doc/site; webgen; scp -r output/* #{RUBY_FORGE_USER}@rubyforge.org:/var/www/gforge-projects/#{RUBY_FORGE_PROJECT}/; popd }
105
+ end
106
+
107
+ begin
108
+ require 'rcov/rcovtask'
109
+ Rcov::RcovTask.new do |t|
110
+ t.test_files = FileList['test/*_test.rb']
111
+ t.output_dir = File.dirname(__FILE__) + "/coverage"
112
+ t.verbose = true
95
113
  end
114
+ rescue Object
96
115
  end
97
116
 
98
- task :reinstall => [:uninstall, :install] do
117
+ task :site_coverage => [:rcov] do
118
+ sh %{ scp -r test/coverage/* #{RUBY_FORGE_USER}@rubyforge.org:/var/www/gforge-projects/#{RUBY_FORGE_PROJECT}/coverage/ }
99
119
  end
100
120
 
101
121
  task :release_files => [:clean, :package] do
@@ -125,7 +145,7 @@ task :publish_news do
125
145
  Rake::XForge::NewsPublisher.new(project) do |publisher|
126
146
  publisher.user_name = RUBY_FORGE_USER
127
147
  publisher.password = ENV['RUBYFORGE_PASSWORD']
128
- publisher.subject = "Cerberus #{PKG_VERSION} Released"
129
- publisher.details = "I am glad to announce first public release of Cerberus tool. Version #{PKG_VERSION} is out. Cerberus is a simple command-line Continuous integration tool for Ruby project. Install Cerberus with 'gem install cerberus' then add project 'cerberus add SVN_URL' and then build it 'cerberus build YOUR_PROJECT_NAME'"
148
+ publisher.subject = "[ANN] Cerberus #{PKG_VERSION} Released"
149
+ publisher.details = IO.read(File.dirname(__FILE__) + '/README')
130
150
  end
131
151
  end
data/doc/OPTIONS ADDED
@@ -0,0 +1,45 @@
1
+ bin_path:
2
+ publisher:
3
+ active:
4
+ mail:
5
+ delivery_method: smtp C
6
+ address: mail.tut.by C
7
+ port: 2525 C
8
+ domain: C
9
+ user_name: anatol C
10
+ password: somepass C
11
+ authentication: login C
12
+ sender: "'Cerberus' <anatol2003@tut.by>" C
13
+ recipients: anatol.pomozov@gmail.com A
14
+ jabber:
15
+ jid: C
16
+ port: C
17
+ password: C
18
+ digest: C
19
+ recipients: A
20
+ irc:
21
+ nick: A
22
+ server: A
23
+ port: A
24
+ channel: A
25
+ rss:
26
+ file:
27
+ scm:
28
+ type: svn A
29
+ url: A
30
+ builder
31
+ type: rake #supported: maven2 A
32
+ rake: C
33
+ task: test CA
34
+ maven2:
35
+
36
+
37
+ L - Only for command line interface
38
+ C - Cerberus config
39
+ A - application level config
40
+
41
+ L
42
+ recipients (add)
43
+ verbose or quite ??(add, build)
44
+ application_name (add)
45
+ scm (add, default svn)
@@ -0,0 +1,2 @@
1
+ class Cerberus::Builder::Maven2
2
+ end
@@ -0,0 +1,2 @@
1
+ class Cerberus::Builder::Rake
2
+ end
data/lib/cerberus/cli.rb CHANGED
@@ -19,17 +19,17 @@ module Cerberus
19
19
  when 'add'
20
20
  path = args.shift || Dir.pwd
21
21
 
22
- command = Cerberus::Add.new(path, cli_options)
22
+ command = Cerberus::AddCommand.new(path, cli_options)
23
23
  command.run
24
24
  when 'build'
25
25
  say HELP if args.empty?
26
26
 
27
27
  application_name = args.shift
28
28
 
29
- command = Cerberus::Build.new(application_name, cli_options)
29
+ command = Cerberus::BuildCommand.new(application_name, cli_options)
30
30
  command.run
31
31
  when 'buildall'
32
- command = Cerberus::BuildAll.new(cli_options)
32
+ command = Cerberus::BuildAllCommand.new(cli_options)
33
33
  command.run
34
34
  end
35
35
  end
@@ -0,0 +1,23 @@
1
+ #Copy this file to config.yml and configure notification services
2
+ publisher:
3
+ active: mail jabber rss
4
+ mail:
5
+ delivery_method: smtp
6
+ address: smtp.mymail.com
7
+ port: 2525
8
+ domain: mymail.com
9
+ user_name: cerberus
10
+ password: somepass
11
+ authentication: plain
12
+ sender: "'Cerberus Builder' <cerberus@mymail.com>"
13
+ jabber:
14
+ jid: cerberus@gtalk.google.com
15
+ port: 5222
16
+ password: mypass
17
+ digest: false
18
+ irc:
19
+ nick: cerb
20
+ server: irc.freenode.net
21
+ channel: cerberus
22
+ rss:
23
+ file: /usr/www/rss.xml
@@ -14,8 +14,13 @@ module Cerberus
14
14
  @config.merge!(cli_options)
15
15
  end
16
16
 
17
- def [](name)
18
- @config[name]
17
+ def [](*path)
18
+ c = @config
19
+ path.each{|p|
20
+ c = c[p]
21
+ return if c.nil?
22
+ }
23
+ c
19
24
  end
20
25
 
21
26
  private
File without changes
@@ -1,4 +1,4 @@
1
1
  module Cerberus
2
2
  HOME = File.expand_path(ENV['CERBERUS_HOME'] || '~/.cerberus')
3
3
  CONFIG_FILE = "#{HOME}/config.yml"
4
- end
4
+ end
@@ -5,10 +5,26 @@ require 'cerberus/utils'
5
5
  require 'cerberus/constants'
6
6
  require 'cerberus/config'
7
7
 
8
- require 'cerberus/notifier/email'
8
+ require 'cerberus/publisher/mail'
9
+ require 'cerberus/publisher/jabber'
10
+ require 'cerberus/publisher/irc'
11
+ require 'cerberus/publisher/rss'
12
+ require 'cerberus/scm/svn'
9
13
 
10
14
  module Cerberus
11
- class Add
15
+ SCM_TYPES = {
16
+ 'svn' => Cerberus::SCM::SVN
17
+ }
18
+
19
+ PUBLISHER_TYPES = {
20
+ :mail => Cerberus::Publisher::Mail,
21
+ :jabber => Cerberus::Publisher::Jabber,
22
+ :irc => Cerberus::Publisher::IRC,
23
+ :rss => Cerberus::Publisher::RSS
24
+ }
25
+
26
+ class AddCommand
27
+ EXAMPLE_CONFIG = File.expand_path(File.dirname(__FILE__) + '/config.example.yml')
12
28
  include Cerberus::Utils
13
29
 
14
30
  def initialize(path, cli_options = {})
@@ -16,19 +32,23 @@ module Cerberus
16
32
  end
17
33
 
18
34
  def run
19
- checkout = Checkout.new(@path, @config)
20
- say "Can't find any svn application under #{@path}" unless checkout.url
35
+ scm_type = @config[:scm] || 'svn'
36
+ say "SCM #{scm_type} not supported" unless SCM_TYPES[scm_type]
37
+
38
+ scm = SCM_TYPES[scm_type].new(@path, @config)
39
+ say "Can't find any #{scm_type} application under #{@path}" unless scm.url
21
40
 
22
41
  application_name = @config[:application_name] || extract_project_name(@path)
23
42
 
24
- create_default_config
43
+ create_example_config
25
44
 
26
45
  config_name = "#{HOME}/config/#{application_name}.yml"
27
46
  say "Application #{application_name} already present in Cerberus" if File.exists?(config_name)
28
47
 
29
- app_config = {
30
- 'url' => checkout.url,
31
- 'recipients' => @config[:recipients]
48
+ app_config = { 'scm' => {
49
+ 'url' => scm.url,
50
+ 'type' => scm_type },
51
+ 'publisher' => {'mail' => {'recipients' => @config[:recipients]}}
32
52
  }
33
53
  dump_yml(config_name, app_config)
34
54
  puts "Application '#{application_name}' was successfully added to Cerberus" unless @config[:quiet]
@@ -39,26 +59,16 @@ module Cerberus
39
59
  path = File.expand_path(path) if test(?d, path)
40
60
  File.basename(path).strip
41
61
  end
42
-
43
- def create_default_config
44
- default_mail_config =
45
- {'mail'=>
46
- { 'delivery_method'=>'smtp',
47
- 'address'=>'somserver.com',
48
- 'port' => 25,
49
- 'domain'=>'somserver.com',
50
- 'user_name'=>'secret_user',
51
- 'password'=>'secret_password',
52
- 'authentication' => 'plain'
53
- },
54
- 'sender' => "'Cerberus' <cerberus@example.com>"}
55
- dump_yml(CONFIG_FILE, default_mail_config, false)
62
+
63
+ def create_example_config
64
+ FileUtils.mkpath(HOME) unless test(?d, HOME)
65
+ FileUtils.cp(EXAMPLE_CONFIG, CONFIG_FILE) unless test(?f, CONFIG_FILE)
56
66
  end
57
67
  end
58
68
 
59
- class Build
69
+ class BuildCommand
60
70
  include Cerberus::Utils
61
- attr_reader :output, :success, :checkout, :status
71
+ attr_reader :output, :success, :scm, :status
62
72
 
63
73
  def initialize(application_name, cli_options = {})
64
74
  unless File.exists?("#{HOME}/config/#{application_name}.yml")
@@ -72,16 +82,16 @@ module Cerberus
72
82
 
73
83
  @status = Status.new("#{app_root}/status.log")
74
84
 
75
- @checkout = Checkout.new(@config[:application_root], @config)
85
+ @scm = SCM_TYPES[@config[:scm, :type] || 'svn'].new(@config[:application_root], @config)
76
86
  end
77
87
 
78
88
  def run
79
89
  begin
80
90
  previous_status = @status.recall
81
- @checkout.update!
91
+ @scm.update!
82
92
 
83
93
  state =
84
- if @checkout.has_changes? or not previous_status
94
+ if @scm.has_changes? or not previous_status
85
95
  if status = make
86
96
  @status.keep(:succesful)
87
97
  case previous_status
@@ -101,14 +111,27 @@ module Cerberus
101
111
  end
102
112
 
103
113
  if [:failure, :broken, :revival, :setup].include?(state)
104
- Cerberus::Notifier::Email.notify(state, self, @config)
114
+ @config[:publisher, :active].split.each do |pub|
115
+ silence_stream(STDOUT) { #some of publishers like IRC very noisy
116
+ clazz = PUBLISHER_TYPES[pub.to_sym]
117
+ if clazz
118
+ clazz.publish(state, self, @config)
119
+ else
120
+ raise "There is no such publisher: #{pub}"
121
+ end
122
+ }
123
+ end
105
124
  end
106
125
  rescue Exception => e
107
- File.open("#{HOME}/work/#{@config[:application_name]}/error.log", File::WRONLY|File::APPEND|File::CREAT) do |f|
108
- f.puts Time.now.strftime("%a, %d %b %Y %H:%M:%S %z")
109
- f.puts e.message
110
- f.puts e.backtrace.collect{|line| ' '*5 + line}
111
- f.puts "\n"
126
+ if ENV['CERBERUS_ENV'] == 'TEST'
127
+ raise e
128
+ else
129
+ File.open("#{HOME}/work/#{@config[:application_name]}/error.log", File::WRONLY|File::APPEND|File::CREAT) do |f|
130
+ f.puts Time.now.strftime("%a, %d %b %Y %H:%M:%S -- #{e.class}")
131
+ f.puts e.message unless e.message.blank?
132
+ f.puts e.backtrace.collect{|line| ' '*5 + line}
133
+ f.puts "\n"
134
+ end
112
135
  end
113
136
  end
114
137
  end
@@ -116,9 +139,7 @@ module Cerberus
116
139
  private
117
140
  def make
118
141
  Dir.chdir @config[:application_root]
119
-
120
- @output = `#{@config[:bin_path]}#{choose_rake_exec()} #{@config[:rake_task]} 2>&1`
121
-
142
+ @output = `#{@config[:bin_path]}#{choose_rake_exec()} #{@config[:builder, :rake, :task]} 2>&1`
122
143
  make_successful?
123
144
  end
124
145
 
@@ -135,7 +156,7 @@ module Cerberus
135
156
 
136
157
  ext.each{|e|
137
158
  begin
138
- out = `rake#{e} --version`
159
+ out = `#{@config[:bin_path]}rake#{e} --version`
139
160
  return "rake#{e}" if out =~ /rake/
140
161
  rescue
141
162
  end
@@ -143,7 +164,7 @@ module Cerberus
143
164
  end
144
165
  end
145
166
 
146
- class BuildAll
167
+ class BuildAllCommand
147
168
  def initialize(cli_options = {})
148
169
  @cli_options = cli_options
149
170
  end
@@ -153,66 +174,12 @@ module Cerberus
153
174
  fn =~ %r{#{HOME}/config/(.*).yml}
154
175
  application_name = $1
155
176
 
156
- command = Cerberus::Build.new(application_name, @cli_options)
177
+ command = Cerberus::BuildCommand.new(application_name, @cli_options)
157
178
  command.run
158
179
  end
159
180
  end
160
181
  end
161
182
 
162
-
163
- class Checkout
164
- def initialize(path, options = {})
165
- raise "Path can't be nil" unless path
166
-
167
- @path, @options = path.strip, options
168
- @encoded_path = (@path.include?(' ') ? "\"#{@path}\"" : @path)
169
- end
170
-
171
- def update!
172
- if test(?d, @path + '/.svn')
173
- @status = execute("svn update")
174
- else
175
- FileUtils.mkpath(@path) unless test(?d,@path)
176
- @status = execute("svn checkout", nil, @options[:url])
177
- end
178
- end
179
-
180
- def has_changes?
181
- @status =~ /[A-Z]\s+[\w\/]+/
182
- end
183
-
184
- def current_revision
185
- info['Revision'].to_i
186
- end
187
-
188
- def url
189
- info['URL']
190
- end
191
-
192
- def last_commit_message
193
- message = execute("svn log", "--limit 1 -v")
194
- #strip first line that contains command line itself (svn log --limit ...)
195
- if ((idx = message.index('-'*72)) != 0 )
196
- message[idx..-1]
197
- else
198
- message
199
- end
200
- end
201
-
202
- def last_author
203
- info['Last Changed Author']
204
- end
205
-
206
- private
207
- def info
208
- @info ||= YAML.load(execute("svn info"))
209
- end
210
-
211
- def execute(command, parameters = nil, pre_parameters = nil)
212
- `#{@options[:bin_path]}#{command} #{pre_parameters} #{@encoded_path} #{parameters}`
213
- end
214
- end
215
-
216
183
  class Status
217
184
  def initialize(path)
218
185
  @path = path
@@ -0,0 +1,29 @@
1
+ require 'cerberus/version'
2
+
3
+ module Cerberus
4
+ module Publisher
5
+ class Base
6
+ def self.formatted_message(state, build, options)
7
+ subject =
8
+ case state
9
+ when :setup
10
+ "Cerberus set up for project (##{build.scm.current_revision})"
11
+ when :broken
12
+ "Build still broken (##{build.scm.current_revision})"
13
+ when :failure
14
+ "Build broken by #{build.scm.last_author} (##{build.scm.current_revision})"
15
+ when :revival
16
+ "Build fixed by #{build.scm.last_author} (##{build.scm.current_revision})"
17
+ else
18
+ raise "Unknown build state #{state}"
19
+ end
20
+
21
+ subject = "[#{options[:application_name]}] #{subject}"
22
+ generated_by = "--\nCerberus #{Cerberus::VERSION::STRING}, http://rubyforge.org/projects/cerberus"
23
+ body = [ build.scm.last_commit_message, build.output, generated_by ].join("\n\n")
24
+
25
+ return subject, body
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,26 @@
1
+ require 'IRC'
2
+ require 'cerberus/publisher/base'
3
+
4
+ class Cerberus::Publisher::IRC < Cerberus::Publisher::Base
5
+ def self.publish(state, build, options)
6
+ irc_options = options[:publisher, :irc]
7
+ subject,body = Cerberus::Publisher::Base.formatted_message(state, build, options)
8
+ message = subject + "\n" + '*' * subject.length + "\n" + body
9
+
10
+
11
+ channel = '#' + irc_options[:channel]
12
+ bot = IRC.new(irc_options[:nick] || 'cerberus', irc_options[:server], irc_options[:port] || 6667)
13
+ IRCEvent.add_callback('endofmotd') { |event|
14
+ bot.add_channel(channel)
15
+ message.split("\n").each{|line|
16
+ bot.send_message(channel, line)
17
+ }
18
+ bot.send_quit
19
+ }
20
+ begin
21
+ bot.connect #Why it always fails?
22
+ rescue Exception => e
23
+ puts e.message
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,32 @@
1
+ require 'jabber4r/jabber4r'
2
+ require 'cerberus/publisher/base'
3
+
4
+ class Cerberus::Publisher::Jabber < Cerberus::Publisher::Base
5
+ def self.publish(state, build, options)
6
+ begin
7
+ jabber_options = options[:publisher, :jabber]
8
+ subject,body = Cerberus::Publisher::Base.formatted_message(state, build, options)
9
+
10
+ session = login(jabber_options[:jid], jabber_options[:password])
11
+ jabber_options[:recipients].split(',').each do |address|
12
+ session.new_message(address.strip).set_subject(subject).set_body(body).send
13
+ end
14
+ ensure
15
+ session.release if session
16
+ end
17
+ end
18
+
19
+ def self.login(id_resource, password, register_if_login_fails=true)
20
+ begin
21
+ session = ::Jabber::Session.bind(id_resource, password)
22
+ rescue
23
+ if(register_if_login_fails)
24
+ if(::Jabber::Session.register(id_resource, password))
25
+ Cerberus::Publisher::Jabber.login(id_resource, password, false)
26
+ else
27
+ raise "Failed to register #{id_resource}"
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,28 @@
1
+ require 'action_mailer'
2
+ require 'cerberus/publisher/base'
3
+
4
+ class Cerberus::Publisher::Mail < Cerberus::Publisher::Base
5
+ def self.publish(state, build, options)
6
+ configure(options[:publisher, :mail].dup)
7
+ ActionMailerPublisher.deliver_message(state, build, options)
8
+ end
9
+
10
+ private
11
+ def self.configure(config)
12
+ [:authentication, :delivery_method].each do |k|
13
+ config[k] = config[k].to_sym if config[k]
14
+ end
15
+
16
+ ActionMailer::Base.delivery_method = config[:delivery_method] if config[:delivery_method]
17
+ ActionMailer::Base.server_settings = config
18
+ end
19
+
20
+ class ActionMailerPublisher < ActionMailer::Base
21
+ def message(state, build, options)
22
+ @subject, @body = Cerberus::Publisher::Base.formatted_message(state, build, options)
23
+ @recipients, @sent_on = options[:publisher, :mail, :recipients], Time.now
24
+ @from = options[:publisher, :mail, :sender] || "'Cerberus' <cerberus@example.com>"
25
+ # raise "Please specify recipient addresses for application '#{options[:application_name]}'" unless options[:recipients]
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,26 @@
1
+ require 'cerberus/publisher/base'
2
+
3
+ class Cerberus::Publisher::RSS < Cerberus::Publisher::Base
4
+ def self.publish(state, build, options)
5
+ config = options[:publisher, :rss]
6
+ subject,body = Cerberus::Publisher::Base.formatted_message(state, build, options)
7
+
8
+ pub_date = Time.now.iso8601
9
+ result = <<-END
10
+ <rss version="2.0">
11
+ <channel>
12
+ <title>Cerberus build feed for #{options[:application_name]}</title>
13
+ <pubDate>#{pub_date}</pubDate>
14
+ <generator>http://rubyforge.org/projects/cerberus</generator>
15
+ <item>
16
+ <title>#{subject}</title>
17
+ <pubDate>#{pub_date}</pubDate>
18
+ <description><pre>#{body}</pre></description>
19
+ </item>
20
+ </channel>
21
+ </rss>
22
+ END
23
+
24
+ File.open(config[:file], 'w'){|f| f.write(result)}
25
+ end
26
+ end
@@ -0,0 +1,2 @@
1
+ class Cerberus::SCM::CVS
2
+ end
@@ -0,0 +1,2 @@
1
+ class Cerberus::SCM::Darcs
2
+ end
@@ -0,0 +1,53 @@
1
+ class Cerberus::SCM::SVN
2
+ def initialize(path, options = {})
3
+ raise "Path can't be nil" unless path
4
+
5
+ @path, @options = path.strip, options
6
+ @encoded_path = (@path.include?(' ') ? "\"#{@path}\"" : @path)
7
+ end
8
+
9
+ def update!
10
+ if test(?d, @path + '/.svn')
11
+ execute("svn cleanup") #TODO check first that it was locked
12
+ @status = execute("svn update")
13
+ else
14
+ FileUtils.mkpath(@path) unless test(?d,@path)
15
+ @status = execute("svn checkout", nil, @options[:scm, :url])
16
+ end
17
+ end
18
+
19
+ def has_changes?
20
+ @status =~ /[A-Z]\s+[\w\/]+/
21
+ end
22
+
23
+ def current_revision
24
+ info['Revision'].to_i
25
+ end
26
+
27
+ def url
28
+ info['URL']
29
+ end
30
+
31
+ def last_commit_message
32
+ message = execute("svn log", "--limit 1 -v")
33
+ #strip first line that contains command line itself (svn log --limit ...)
34
+ if ((idx = message.index('-'*72)) != 0 )
35
+ message[idx..-1]
36
+ else
37
+ message
38
+ end
39
+ end
40
+
41
+ def last_author
42
+ info['Last Changed Author']
43
+ end
44
+
45
+ private
46
+ def info
47
+ @info ||= YAML.load(execute("svn info"))
48
+ end
49
+
50
+ def execute(command, parameters = nil, pre_parameters = nil)
51
+ `#{@options[:bin_path]}#{command} #{pre_parameters} #{@encoded_path} #{parameters}`
52
+ end
53
+ end
@@ -27,4 +27,13 @@ module Cerberus
27
27
  File.exists?(file_name) ? YAML::load(IO.read(file_name)) : default
28
28
  end
29
29
  end
30
+ end
31
+
32
+ alias __exec `
33
+ def `(cmd)
34
+ begin
35
+ __exec(cmd)
36
+ rescue Exception => e
37
+ raise "Unable to execute: #{cmd}"
38
+ end
30
39
  end
@@ -1,8 +1,8 @@
1
1
  module Cerberus
2
2
  module VERSION #:nodoc:
3
3
  MAJOR = 0
4
- MINOR = 1
5
- TINY = 1
4
+ MINOR = 2
5
+ TINY = 0
6
6
 
7
7
  STRING = [MAJOR, MINOR, TINY].join('.')
8
8
  end
data/test/config_test.rb CHANGED
@@ -1,4 +1,5 @@
1
- require 'test_helper'
1
+ require File.dirname(__FILE__) + '/test_helper'
2
+
2
3
  require 'cerberus/config'
3
4
 
4
5
  class ConfigTest < Test::Unit::TestCase
@@ -1,10 +1,11 @@
1
- require 'test_helper'
1
+ require File.dirname(__FILE__) + '/test_helper'
2
2
 
3
3
  require 'cerberus/cli'
4
4
 
5
5
  class FunctionalTest < Test::Unit::TestCase
6
6
  def setup
7
7
  FileUtils.rm_rf HOME
8
+ ActionMailer::Base.deliveries.clear
8
9
  end
9
10
 
10
11
  def teardown
@@ -15,11 +16,13 @@ class FunctionalTest < Test::Unit::TestCase
15
16
  def test_add_by_url
16
17
  assert !File.exists?(HOME + '/config/svn_repo.yml')
17
18
 
18
- command = Cerberus::Add.new(" #{SVN_URL} ", :quiet => true)
19
+ command = Cerberus::AddCommand.new(" #{SVN_URL} ", :quiet => true)
19
20
  command.run
20
21
 
21
22
  assert File.exists?(HOME + '/config/svn_repo.yml')
22
- assert_equal SVN_URL, load_yml(HOME + '/config/svn_repo.yml')['url']
23
+ scm_conf = load_yml(HOME + '/config/svn_repo.yml')['scm']
24
+ assert_equal 'svn', scm_conf['type']
25
+ assert_equal SVN_URL, scm_conf['url']
23
26
 
24
27
  assert File.exists?(HOME + '/config.yml')
25
28
  end
@@ -27,13 +30,15 @@ class FunctionalTest < Test::Unit::TestCase
27
30
  def test_add_by_dir
28
31
  sources_dir = File.dirname(__FILE__) + '/..'
29
32
 
30
- command = Cerberus::Add.new(sources_dir, :quiet => true)
33
+ command = Cerberus::AddCommand.new(sources_dir, :quiet => true)
31
34
  command.run
32
35
 
33
36
  project_config = HOME + "/config/#{File.basename(File.expand_path(sources_dir))}.yml" #name of added application should be calculated from File System path
34
37
 
35
38
  assert File.exists?(project_config)
36
- assert_match %r{svn(\+ssh)?://(\w+@)?rubyforge.org/var/svn/cerberus}, load_yml(project_config)['url']
39
+ scm_conf = load_yml(project_config)['scm']
40
+ assert_equal 'svn', scm_conf['type']
41
+ assert_match %r{svn(\+ssh)?://(\w+@)?rubyforge.org/var/svn/cerberus}, scm_conf['url']
37
42
 
38
43
  assert File.exists?(HOME + '/config.yml')
39
44
  end
@@ -41,7 +46,7 @@ class FunctionalTest < Test::Unit::TestCase
41
46
  def test_build
42
47
  add_application('myapp', SVN_URL)
43
48
 
44
- build = Cerberus::Build.new('myapp')
49
+ build = Cerberus::BuildCommand.new('myapp')
45
50
  build.run
46
51
  assert_equal 1, ActionMailer::Base.deliveries.size #first email that project was setup
47
52
 
@@ -50,13 +55,13 @@ class FunctionalTest < Test::Unit::TestCase
50
55
  assert_equal 'succesful', IO.read(status_file)
51
56
 
52
57
  FileUtils.rm status_file
53
- build = Cerberus::Build.new('myapp')
58
+ build = Cerberus::BuildCommand.new('myapp')
54
59
  build.run
55
60
  assert File.exists?(status_file)
56
61
 
57
62
  assert_equal 2, ActionMailer::Base.deliveries.size #first email that project was setup
58
63
 
59
- build = Cerberus::Build.new('myapp')
64
+ build = Cerberus::BuildCommand.new('myapp')
60
65
  build.run
61
66
  assert_equal 2, ActionMailer::Base.deliveries.size #Number of mails not changed
62
67
 
@@ -64,7 +69,7 @@ class FunctionalTest < Test::Unit::TestCase
64
69
  #remove status file to run project again
65
70
  FileUtils.rm status_file
66
71
  add_test_case_to_project('myapp', 'assert false') { #if assertion failed
67
- build = Cerberus::Build.new('myapp')
72
+ build = Cerberus::BuildCommand.new('myapp')
68
73
  build.run
69
74
 
70
75
  assert_equal 'failed', IO.read(status_file)
@@ -75,7 +80,7 @@ class FunctionalTest < Test::Unit::TestCase
75
80
  #remove status file to run project again
76
81
  FileUtils.rm status_file
77
82
  add_test_case_to_project('myapp', 'raise "Some exception here"') { #if we have exception
78
- build = Cerberus::Build.new('myapp')
83
+ build = Cerberus::BuildCommand.new('myapp')
79
84
  build.run
80
85
 
81
86
  assert_equal 'failed', IO.read(status_file)
@@ -85,20 +90,20 @@ class FunctionalTest < Test::Unit::TestCase
85
90
  def test_have_no_awkward_header
86
91
  add_application('myapp', SVN_URL)
87
92
 
88
- build = Cerberus::Build.new('myapp')
93
+ build = Cerberus::BuildCommand.new('myapp')
89
94
  build.run
90
95
 
91
- assert build.checkout.last_commit_message !~ /-rHEAD -v/
92
- assert_equal 0, build.checkout.last_commit_message.index('-' * 72)
96
+ assert build.scm.last_commit_message !~ /-rHEAD -v/
97
+ assert_equal 0, build.scm.last_commit_message.index('-' * 72)
93
98
  end
94
99
 
95
- def test_have_no_awkward_header
100
+ def test_batch_running
96
101
  add_application('myapp1', SVN_URL)
97
102
  add_application('myapp2', SVN_URL)
98
103
  add_application('myapp3', SVN_URL)
99
104
  add_application('myapp4', SVN_URL)
100
105
 
101
- build = Cerberus::BuildAll.new
106
+ build = Cerberus::BuildAllCommand.new
102
107
  build.run
103
108
 
104
109
  for i in 1..4 do
@@ -1,4 +1,4 @@
1
- require 'test_helper'
1
+ require File.dirname(__FILE__) + '/test_helper'
2
2
 
3
3
  require 'yaml'
4
4
 
@@ -15,13 +15,13 @@ class IntegrationTest < Test::Unit::TestCase
15
15
  output = run_cerb(" add #{SVN_URL} ")
16
16
  assert_match /was successfully added/, output
17
17
  assert File.exists?(HOME + '/config/svn_repo.yml')
18
- assert_equal SVN_URL, load_yml(HOME + '/config/svn_repo.yml')['url']
18
+ assert_equal SVN_URL, load_yml(HOME + '/config/svn_repo.yml')['scm']['url']
19
19
 
20
20
  #try to add second time
21
21
  output = run_cerb("add #{SVN_URL}")
22
22
  assert_match /already present/, output
23
23
  assert File.exists?(HOME + '/config/svn_repo.yml')
24
- assert_equal SVN_URL, load_yml(HOME + '/config/svn_repo.yml')['url']
24
+ assert_equal SVN_URL, load_yml(HOME + '/config/svn_repo.yml')['scm']['url']
25
25
  end
26
26
 
27
27
  def test_add_project_with_parameters
@@ -31,8 +31,8 @@ class IntegrationTest < Test::Unit::TestCase
31
31
  assert File.exists?(HOME + '/config/hello_world.yml')
32
32
  cfg = load_yml(HOME + '/config/hello_world.yml')
33
33
 
34
- assert_equal SVN_URL, cfg['url']
35
- assert_equal 'aa@gmail.com', cfg['recipients']
34
+ assert_equal SVN_URL, cfg['scm']['url']
35
+ assert_equal 'aa@gmail.com', cfg['publisher']['mail']['recipients']
36
36
  end
37
37
 
38
38
  def test_run_project
@@ -0,0 +1,17 @@
1
+ require File.dirname(__FILE__) + '/test_helper'
2
+
3
+ require 'cerberus/publisher/irc'
4
+ require 'cerberus/config'
5
+ require 'mock/irc'
6
+ require 'mock/build'
7
+
8
+ class IRCPublisherTest < Test::Unit::TestCase
9
+ def test_publisher
10
+ options = Cerberus::Config.new(nil, :publisher => {:irc => {:channel => 'hello'}}, :application_name => 'IrcApp')
11
+ build = DummyBuild.new('last message', 'this is output', 1232, 'anatol')
12
+
13
+ Cerberus::Publisher::IRC.publish(:setup, build, options)
14
+
15
+ assert IRCConnection.connected
16
+ end
17
+ end
@@ -0,0 +1,20 @@
1
+ require File.dirname(__FILE__) + '/test_helper'
2
+
3
+ require 'cerberus/publisher/jabber'
4
+ require 'mock/jabber4r'
5
+ require 'mock/build'
6
+
7
+ class JabberPublisherTest < Test::Unit::TestCase
8
+ def test_publisher
9
+ options = Cerberus::Config.new(nil, :publisher => {:jabber => {:recipients => ' jit1@google.com, another@google.com '}}, :application_name => 'MegaApp')
10
+ build = DummyBuild.new('last message', 'this is output', 1232, 'anatol')
11
+
12
+ Cerberus::Publisher::Jabber.publish(:setup, build, options)
13
+
14
+ messages = Jabber::Protocol::Message.messages
15
+ assert_equal 2, messages.size
16
+ assert_equal 'google.com', messages[0].to.host
17
+ assert_equal 'jit1', messages[0].to.node
18
+ assert_equal '[MegaApp] Cerberus set up for project (#1232)', messages[0].subject
19
+ end
20
+ end
@@ -0,0 +1,25 @@
1
+ require File.dirname(__FILE__) + '/test_helper'
2
+
3
+ require 'cerberus/publisher/mail'
4
+ require 'mock/build'
5
+
6
+ class MailPublisherTest < Test::Unit::TestCase
7
+ def setup
8
+ ActionMailer::Base.deliveries.clear
9
+ end
10
+
11
+ def test_publisher
12
+ options = Cerberus::Config.new(nil, :publisher => {
13
+ :mail => {:recipients => 'anatol.pomozov@hello.com', :sender => 'haha', :delivery_method => 'test'}},
14
+ :application_name => 'MyApp')
15
+ build = DummyBuild.new('last message', 'this is output', 1232, 'anatol')
16
+
17
+ Cerberus::Publisher::Mail.publish(:setup, build, options)
18
+
19
+ mails = ActionMailer::Base.deliveries
20
+ assert_equal 1, mails.size
21
+ mail = mails[0]
22
+ assert_equal 'haha', mail.from_addrs[0].address
23
+ assert_equal '[MyApp] Cerberus set up for project (#1232)', mail.subject
24
+ end
25
+ end
@@ -0,0 +1,10 @@
1
+ class DummyBuild
2
+ attr_reader :output, :scm
3
+ SCM = Struct.new(:last_commit_message, :current_revision, :last_author)
4
+
5
+ def initialize(last_commit_message, output, current_revision, last_author)
6
+ @output = output
7
+
8
+ @scm = SCM.new(last_commit_message, current_revision, last_author)
9
+ end
10
+ end
data/test/mock/irc.rb ADDED
@@ -0,0 +1,20 @@
1
+ class IRCConnection
2
+ @@messages = []
3
+ @@connected = false
4
+
5
+ def self.messages
6
+ @@messages
7
+ end
8
+
9
+ def self.connected
10
+ @@connected
11
+ end
12
+
13
+ def self.send_to_server(msg)
14
+ @@messages << msg
15
+ end
16
+
17
+ def self.handle_connection(server, port)
18
+ @@connected = true
19
+ end
20
+ end
@@ -0,0 +1,24 @@
1
+ class Jabber::Session
2
+ def self.bind(jid, password)
3
+ Jabber::Session.new
4
+ end
5
+
6
+ def initialize
7
+ end
8
+ end
9
+
10
+ class Jabber::Protocol::Message
11
+ @@messages = []
12
+
13
+ def self.messages
14
+ @@messages
15
+ end
16
+
17
+ def self.clear
18
+ @@messages = []
19
+ end
20
+
21
+ def send
22
+ @@messages << self
23
+ end
24
+ end
@@ -0,0 +1,19 @@
1
+ require File.dirname(__FILE__) + '/test_helper'
2
+
3
+ require 'cerberus/publisher/rss'
4
+ require 'mock/build'
5
+ require 'tempfile'
6
+
7
+ class RSSPublisherTest < Test::Unit::TestCase
8
+ def test_publisher
9
+ rss_file = tf = Tempfile.new('cerberus-rss')
10
+ options = Cerberus::Config.new(nil, :publisher => {:rss => {:file => rss_file.path}}, :application_name => 'RSSApp')
11
+ build = DummyBuild.new('last message', 'this is output', 1235, 'anatol')
12
+
13
+ Cerberus::Publisher::RSS.publish(:setup, build, options)
14
+
15
+ xml = REXML::Document.new(IO.read(rss_file.path))
16
+
17
+ assert_equal '[RSSApp] Cerberus set up for project (#1235)', xml.elements["rss/channel/item/title/"].get_text.value
18
+ end
19
+ end
data/test/test_helper.rb CHANGED
@@ -1,4 +1,6 @@
1
1
  $:.unshift File.dirname(__FILE__) + '/../lib'
2
+
3
+ require 'rubygems'
2
4
  require 'test/unit'
3
5
  require 'fileutils'
4
6
 
@@ -14,6 +16,7 @@ class Test::Unit::TestCase
14
16
 
15
17
  HOME = TEMP_DIR + '/home'
16
18
  ENV['CERBERUS_HOME'] = HOME
19
+ ENV['CERBERUS_ENV'] = 'TEST'
17
20
 
18
21
  def self.refresh_subversion
19
22
  FileUtils.rm_rf TEMP_DIR
@@ -47,10 +50,15 @@ end"
47
50
  end
48
51
 
49
52
  def add_application(app_name, url, options = {})
50
- opt = options.dup
51
- opt['url'] = url
52
- opt['recipients'] = 'somebody@com.com'
53
- opt['mail'] = {'delivery_method' => 'test'}
53
+ opt = options.merge(
54
+ 'scm'=>{'url'=>url},
55
+ 'publisher'=>{
56
+ 'active' => 'mail',
57
+ 'mail'=>{'recipients'=>'somebody@com.com', 'delivery_method' => 'test'}
58
+ })
59
+
54
60
  dump_yml(HOME + "/config/#{app_name}.yml", opt)
55
61
  end
56
- end
62
+ end
63
+
64
+ require 'cerberus/config'
metadata CHANGED
@@ -3,8 +3,8 @@ rubygems_version: 0.9.0
3
3
  specification_version: 1
4
4
  name: cerberus
5
5
  version: !ruby/object:Gem::Version
6
- version: 0.1.1
7
- date: 2006-07-22 00:00:00 +04:00
6
+ version: 0.2.0
7
+ date: 2006-08-11 00:00:00 +04:00
8
8
  summary: Cerberus is a Continuous Integration tool that could be easily run from Cron.
9
9
  require_paths:
10
10
  - lib
@@ -30,27 +30,50 @@ authors:
30
30
  - Anatol Pomozov
31
31
  files:
32
32
  - bin/cerberus
33
- - doc/FAQ
34
33
  - lib/cerberus
34
+ - lib/cerberus/builder
35
35
  - lib/cerberus/cli.rb
36
+ - lib/cerberus/config.example.yml
36
37
  - lib/cerberus/config.rb
38
+ - lib/cerberus/config_migration.rb
37
39
  - lib/cerberus/constants.rb
38
40
  - lib/cerberus/latch.rb
39
41
  - lib/cerberus/manager.rb
40
- - lib/cerberus/notifier
42
+ - lib/cerberus/publisher
43
+ - lib/cerberus/scm
41
44
  - lib/cerberus/utils.rb
42
45
  - lib/cerberus/version.rb
43
- - lib/cerberus/notifier/email.rb
46
+ - lib/cerberus/builder/maven2.rb
47
+ - lib/cerberus/builder/rake.rb
48
+ - lib/cerberus/publisher/base.rb
49
+ - lib/cerberus/publisher/irc.rb
50
+ - lib/cerberus/publisher/jabber.rb
51
+ - lib/cerberus/publisher/mail.rb
52
+ - lib/cerberus/publisher/rss.rb
53
+ - lib/cerberus/scm/cvs.rb
54
+ - lib/cerberus/scm/darcs.rb
55
+ - lib/cerberus/scm/svn.rb
44
56
  - test/config_test.rb
45
57
  - test/data
46
58
  - test/functional_test.rb
47
59
  - test/integration_test.rb
60
+ - test/irc_publisher_test.rb
61
+ - test/jabber_publisher_test.rb
62
+ - test/mail_publisher_test.rb
63
+ - test/mock
64
+ - test/rss_publisher_test.rb
48
65
  - test/test_helper.rb
49
66
  - test/data/application.dump
67
+ - test/mock/build.rb
68
+ - test/mock/irc.rb
69
+ - test/mock/jabber4r.rb
50
70
  - LICENSE
51
71
  - README
52
72
  - CHANGES
53
73
  - Rakefile
74
+ - doc/FAQ
75
+ - doc/OPTIONS
76
+ - doc/site
54
77
  test_files:
55
78
  - test/integration_test.rb
56
79
  rdoc_options:
@@ -72,7 +95,7 @@ dependencies:
72
95
  requirements:
73
96
  - - ">="
74
97
  - !ruby/object:Gem::Version
75
- version: 1.2.1
98
+ version: 1.2.3
76
99
  version:
77
100
  - !ruby/object:Gem::Dependency
78
101
  name: rake
@@ -83,3 +106,21 @@ dependencies:
83
106
  - !ruby/object:Gem::Version
84
107
  version: 0.7.1
85
108
  version:
109
+ - !ruby/object:Gem::Dependency
110
+ name: jabber4r
111
+ version_requirement:
112
+ version_requirements: !ruby/object:Gem::Version::Requirement
113
+ requirements:
114
+ - - ">="
115
+ - !ruby/object:Gem::Version
116
+ version: 0.8.0
117
+ version:
118
+ - !ruby/object:Gem::Dependency
119
+ name: Ruby-IRC
120
+ version_requirement:
121
+ version_requirements: !ruby/object:Gem::Version::Requirement
122
+ requirements:
123
+ - - ">="
124
+ - !ruby/object:Gem::Version
125
+ version: 1.0.3
126
+ version:
@@ -1,47 +0,0 @@
1
- require 'action_mailer'
2
-
3
- module Cerberus
4
- module Notifier
5
- class Email
6
- def self.notify(state, build, options)
7
- Email.configure(options)
8
- ActionMailerNotifier.deliver_message(state, build, options)
9
- end
10
-
11
- private
12
- def self.configure(options)
13
- mail_config = options[:mail] || {}
14
- [:authentication, :delivery_method].each do |k|
15
- mail_config[k] = mail_config[k].to_sym if mail_config[k]
16
- end
17
-
18
- ActionMailer::Base.delivery_method = mail_config[:delivery_method] if mail_config[:delivery_method]
19
- ActionMailer::Base.server_settings = mail_config
20
- end
21
-
22
- class ActionMailerNotifier < ActionMailer::Base
23
- def message(state, build, options)
24
- subject =
25
- case state
26
- when :setup
27
- "Cerberus set up for project (##{build.checkout.current_revision})"
28
- when :broken
29
- "Build still broken (##{build.checkout.current_revision})"
30
- when :failure
31
- "Build broken by #{build.checkout.last_author} (##{build.checkout.current_revision})"
32
- when :revival
33
- "Build fixed by #{build.checkout.last_author} (##{build.checkout.current_revision})"
34
- end
35
-
36
- @subject = "[#{options[:application_name]}] #{subject}"
37
- @body = [ build.checkout.last_commit_message, build.output ].join("\n\n")
38
-
39
- @recipients, @sent_on = options[:recipients], Time.now
40
-
41
- @from = options[:sender] || "'Cerberus' <cerberus@example.com>"
42
- raise "Please specify recipient addresses for application '#{options[:application_name]}'" unless options[:recipients]
43
- end
44
- end
45
- end
46
- end
47
- end