snake_eyes 0.1.3

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/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Chris Wanstrath
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,212 @@
1
+ Snake Eyes
2
+ ==========
3
+
4
+ Snake Eyes adds [ninja power](http://drmcninja.com/) to [CI Joe](http://github.com/defunkt/cijoe).
5
+
6
+ ![Ninja Power](http://s3.amazonaws.com/giles/mc_ninja_102609/300px-McNinja.png)
7
+
8
+ CI Joe
9
+ ======
10
+
11
+ Joe is a [Continuous
12
+ Integration](http://en.wikipedia.org/wiki/Continuous_integration)
13
+ server that'll run your tests on demand and report their pass/fail status.
14
+
15
+ Because knowing is half the battle.
16
+
17
+ ![The Battle](http://img.skitch.com/20090805-g4a2qhttwij8n2jr9t552efn3k.png)
18
+
19
+ Quickstart
20
+ ----------
21
+
22
+ Rip:
23
+
24
+ $ rip install git://github.com/defunkt/cijoe.git
25
+ $ git clone git://github.com/you/yourrepo.git
26
+ $ cijoe yourrepo
27
+
28
+ Gemcutter:
29
+
30
+ $ gem install cijoe
31
+ $ git clone git://github.com/you/yourrepo.git
32
+ $ cijoe yourrepo
33
+
34
+ Boom. Navigate to http://localhost:4567 to see Joe in action.
35
+ Check `cijoe -h` for other options.
36
+
37
+ Basically you need to run `cijoe` and hand it the path to a git
38
+ repo. Make sure this isn't a shared repo: Joe needs to own it.
39
+
40
+ Joe looks for various git config settings in the repo you hand it. For
41
+ instance, you can tell Joe what command to run by setting
42
+ `cijoe.runner`:
43
+
44
+ $ git config --add cijoe.runner "rake -s test:units"
45
+
46
+ Joe doesn't care about Ruby, Python, or whatever. As long as the
47
+ runner returns a non-zero exit status on fail and a zero on success,
48
+ everyone is happy.
49
+
50
+ Need to do some massaging of your repo before the tests run, like
51
+ maybe swapping in a new database.yml? No problem - Joe will try to
52
+ run `.git/hooks/after-reset` if it exists before each build phase.
53
+ Do it in there. Just make sure it's executable.
54
+
55
+ Want to notify IRC or email on test pass or failure? Joe will run
56
+ `.git/hooks/build-failed` or `.git/hooks/build-worked` if they exist
57
+ and are executable on build pass / fail. They're just shell scripts -
58
+ put whatever you want in there.
59
+
60
+ Tip: your repo's `HEAD` will point to the commit used to run the
61
+ build. Pull any metadata you want out of that scro.
62
+
63
+
64
+ Other Branches
65
+ --------------
66
+
67
+ Want Joe to run against a branch other than `master`? No problem:
68
+
69
+ $ git config --add cijoe.branch deploy
70
+
71
+
72
+ Notifiers
73
+ ---------
74
+
75
+ CI Joe includes Campfire notification, because it's what they use at GitHub,
76
+ where CI Joe came into being. Want Joe to notify your Campfire? Put this in
77
+ your repo's `.git/config`:
78
+
79
+ [campfire]
80
+ user = your@campfire.email
81
+ pass = passw0rd
82
+ subdomain = whatever
83
+ room = Awesomeness
84
+ ssl = false
85
+
86
+ Or do it the old-fashioned way:
87
+
88
+ $ cd yourrepo
89
+ $ git config --add campfire.user chris@ozmm.org
90
+ $ git config --add campfire.subdomain github
91
+ etc.
92
+
93
+ Snake Eyes gives you an additional option: Gmail. In `.git/config`:
94
+
95
+ [gmail]
96
+ user = your_ci_joe@email
97
+ pass = passw0rd
98
+ recipient = developers@your-company.com
99
+
100
+ Or:
101
+
102
+ $ cd yourrepo
103
+ $ git config --add campfire.user your.ci.server@gmail.com
104
+ $ git config --add campfire.password s3cr3t
105
+ etc
106
+
107
+ If it's not obvious, you do in fact need to have this gmail account and
108
+ the password does in fact need to be valid.
109
+
110
+ CI Joe gives you Campfire (and only Campfire) by default, but Snake Eyes
111
+ makes you specify your notifier in your project's `.git/config`. Sorry for
112
+ the extra work.
113
+
114
+ [cijoe]
115
+ notifier = CIJoe::Gmail
116
+
117
+ Or:
118
+
119
+ [cijoe]
120
+ notifier = CIJoe::Campfire
121
+
122
+ Extending Snake Eyes to support additional notifiers is very, very easy.
123
+ Even though Chris Wanstraäth, CI Joe's author, was very, very explicit
124
+ about not supporting any kind of notifier except the Campfire notifier,
125
+ his code furnishes an API which is very, very extensible and very, very
126
+ friendly to repurposing.
127
+
128
+ The CI Joe Campfire notifier uses a `valid_config?` method to check that the
129
+ notifier will be able to work. If so, it loads the Campfire module right into
130
+ CI Joe, so that when CI Joe calls `notify`, it's calling the `notify` on the
131
+ Campfire module. If you're creating your own notifier module, all you need
132
+ to support is an `activate` method on the module itself and a `notify` instance
133
+ method. Copying the `valid_config?` pattern is strongly advised but absolutely
134
+ not required.
135
+
136
+ Warning: Snake Eyes enables arbitrary notifiers using a language feature
137
+ called `eval`. Many people believe `eval` is evil. All who know it fear
138
+ its power. Snake Eyes is comfortable with `eval` because Snake Eyes is a
139
+ fucking ninja. If you cut yourself on a sharp piece of `eval`, don't come crying
140
+ to Snake Eyes. You'll never find him. Because he's a ninja.
141
+
142
+ Multiple Projects
143
+ -----------------
144
+
145
+ Want CI for multiple projects? Just start multiple instances of Joe!
146
+ He can run on any port - try `cijoe -h` for more options.
147
+
148
+
149
+ HTTP Auth
150
+ ---------
151
+
152
+ Worried about people triggering your builds? Setup HTTP auth:
153
+
154
+ $ git config --add cijoe.user chris
155
+ $ git config --add cijoe.pass secret
156
+
157
+
158
+ GitHub Integration
159
+ ------------------
160
+
161
+ Any POST to Joe will trigger a build. If you are hiding Joe behind
162
+ HTTP auth, that's okay - GitHub knows how to authenticate properly.
163
+
164
+ ![Post-Receive URL](http://img.skitch.com/20090806-d2bxrk733gqu8m11tf4kyir5d8.png)
165
+
166
+ You can find the Post-Receive option under the 'Service Hooks' subtab
167
+ of your project's "Admin" tab.
168
+
169
+
170
+ Daemonize
171
+ ---------
172
+
173
+ Want to run Joe as a daemon? Use `nohup`:
174
+
175
+ $ nohup cijoe -p 4444 repo &
176
+
177
+
178
+ Other CI Servers
179
+ ----------------
180
+
181
+ Need more features? Check out one of these bad boys:
182
+
183
+ * [Cerberus](http://cerberus.rubyforge.org/)
184
+ * [Integrity](http://integrityapp.com/)
185
+ * [CruiseControl.rb](http://cruisecontrolrb.thoughtworks.com/)
186
+ * [BuildBot](http://buildbot.net/trac)
187
+
188
+ Need less notifiers? Check out the original CI Joe:
189
+
190
+ * [CI Joe](http://github.com/defunkt/cijoe)
191
+
192
+
193
+ Screenshots
194
+ -----------
195
+
196
+ ![Building](http://img.skitch.com/20090806-ryw34ksi5ixnrdwxcptqy28iy7.png)
197
+
198
+ ![Built](http://img.skitch.com/20090806-f7j3r65yecaq13hdcxqwtc5krd.)
199
+
200
+
201
+ Questions? Concerns?
202
+ --------------------
203
+
204
+ For CI Joe:
205
+
206
+ [Issues](http://github.com/defunkt/cijoe/issues) or [the mailing list](http://groups.google.com/group/cijoe).
207
+
208
+ ( Chris Wanstrath :: chris@ozmm.org )
209
+
210
+ For Snake Eyes: do not attempt to contact Snake Eyes. Snake Eyes is a ninja.
211
+
212
+ ( Giles Bowkett :: gilesb@gmail.com )
@@ -0,0 +1,25 @@
1
+ desc "Build a gem"
2
+ task :gem => [ :gemspec, :build ]
3
+
4
+ begin
5
+ require 'jeweler'
6
+
7
+ $LOAD_PATH.unshift File.dirname(__FILE__) + '/lib'
8
+ require 'cijoe/version'
9
+
10
+ Jeweler::Tasks.new do |gemspec|
11
+ gemspec.name = "snake_eyes"
12
+ gemspec.summary = "Snake Eyes is a fork of CI Joe, a simple Continuous Integration server."
13
+ gemspec.description = "Snake Eyes is a fork of CI Joe, a simple Continuous Integration server."
14
+ gemspec.email = "gilesb@gmail.com"
15
+ gemspec.homepage = "http://github.com/gilesbowkett/snake_eyes"
16
+ gemspec.authors = ["Chris Wanstrath", "Giles Bowkett", "Dr. McNinja"]
17
+ gemspec.add_dependency 'choice'
18
+ gemspec.add_dependency 'sinatra'
19
+ gemspec.add_dependency 'open4'
20
+ gemspec.version = CIJoe::Version.to_s
21
+ end
22
+ rescue LoadError
23
+ puts "Jeweler not available."
24
+ puts "Install it with: gem install jeweler"
25
+ end
@@ -0,0 +1,49 @@
1
+ #!/usr/bin/env ruby
2
+ $LOAD_PATH.unshift File.dirname(__FILE__) + '/../lib'
3
+
4
+ require 'cijoe'
5
+ require 'choice'
6
+
7
+ Choice.options do
8
+ banner "Usage: #{File.basename(__FILE__)} [-hpv] path_to_git_repo"
9
+ header ''
10
+ header 'Server options:'
11
+
12
+ option :host do
13
+ d = "0.0.0.0"
14
+ short '-h'
15
+ long '--host=HOST'
16
+ desc "The hostname or ip of the host to bind to (default #{d})"
17
+ default d
18
+ end
19
+
20
+ option :port do
21
+ d = 4567
22
+ short '-p'
23
+ long '--port=PORT'
24
+ desc "The port to listen on (default #{d})"
25
+ cast Integer
26
+ default d
27
+ end
28
+
29
+ separator ''
30
+ separator 'Common options: '
31
+
32
+ option :help do
33
+ long '--help'
34
+ desc 'Show this message'
35
+ end
36
+
37
+ option :version do
38
+ short '-v'
39
+ long '--version'
40
+ desc 'Show version'
41
+ action do
42
+ puts "#{File.basename(__FILE__)} v#{CIJoe::Version}"
43
+ exit
44
+ end
45
+ end
46
+ end
47
+
48
+ options = Choice.choices
49
+ CIJoe::Server.start(options[:host], options[:port], Choice.rest[0])
@@ -0,0 +1,5 @@
1
+ git://github.com/collectiveidea/tinder.git 1.2.0
2
+ git://github.com/sinatra/sinatra.git 0.9.4
3
+ git://github.com/rack/rack.git 1.0
4
+ git://github.com/defunkt/choice.git 8b125564
5
+ git://github.com/ahoward/open4.git 25c3ed8
@@ -0,0 +1,53 @@
1
+ #!/bin/sh
2
+ ### BEGIN INIT INFO
3
+ # Provides: cijoe
4
+ # Required-Start: $syslog $local_fs $network
5
+ # Required-Stop: $syslog $local_fs $network
6
+ # Default-Start: 2 3 4 5
7
+ # Default-Stop: 0 1
8
+ # Description: Run the CIJoe CI server. Yo Joe!!
9
+ ### END INIT INFO
10
+
11
+ . /lib/lsb/init-functions
12
+
13
+ REPO=/path/to/your/git/repository
14
+ PORT=4567
15
+
16
+ NAME=cijoe
17
+ INSTALL_DIR=/usr/sbin
18
+ DAEMON=$INSTALL_DIR/$NAME
19
+ DAEMON_ARGS="-p $PORT $REPO"
20
+ PIDFILE=/var/run/$NAME.pid
21
+ DAEMON_USER=www-data
22
+ DAEMON_GROUP=$DAEMON_USER
23
+
24
+ # test -f $DAEMON || exit 0
25
+ # test -f $PROJECT_DIR || exit 0
26
+
27
+ case "$1" in
28
+ start)
29
+ log_daemon_msg "Starting cijoe" "cijoe"
30
+ start-stop-daemon --background --make-pidfile --exec $DAEMON --start --name $NAME --pidfile $PIDFILE --chuid $DAEMON_USER:$DAEMON_GROUP -- $DAEMON_ARGS
31
+ log_end_msg $?
32
+ ;;
33
+ stop)
34
+ log_daemon_msg "Stopping cijoe" "cijoe"
35
+ start-stop-daemon --stop --pidfile $PIDFILE --quiet --retry 10
36
+ log_end_msg $?
37
+ ;;
38
+ restart)
39
+ log_daemon_msg "Restarting cijoe" "cijoe"
40
+ start-stop-daemon --stop --pidfile $PIDFILE --quiet --retry 10
41
+ start-stop-daemon --background --make-pidfile --exec $DAEMON --start --name $NAME --pidfile $PIDFILE --chuid $DAEMON_USER:$DAEMON_GROUP -- $DAEMON_ARGS
42
+ log_end_msg $?
43
+ ;;
44
+ status)
45
+ status_of_proc $DAEMON $NAME && exit 0 || exit $?
46
+ ;;
47
+ *)
48
+ log_action_msg "Usage: /etc/init.d/cijoe (start|stop|restart)"
49
+ exit 2
50
+ ;;
51
+ esac
52
+
53
+ exit 0
@@ -0,0 +1,148 @@
1
+ ##
2
+ # Chris said:
3
+ # Seriously, I'm gonna be nuts about keeping this simple.
4
+ #
5
+ # Cool.
6
+ #
7
+ # He also said:
8
+ # It only notifies to Campfire.
9
+ #
10
+ # Wait. What? No. I need it notifying via Gmail.
11
+ #
12
+ # And thus was born Snake Eyes, which brings ninja power to CI Joe.
13
+ #
14
+ # Specifically, the ninja power of Gmail. And of arbitrary notifiers
15
+ # to anywhere, which really required very very little effort. Despite Chris'
16
+ # anti-other-notifiers stance, he wrote one of the easiest APIs to extend
17
+ # to other notifiers that I've ever seen.
18
+ #
19
+ # btw, I suppose I should have called it Cnake Eyes, but it's too late now.
20
+
21
+ begin
22
+ require 'open4'
23
+ rescue LoadError
24
+ abort "** Please install open4"
25
+ end
26
+
27
+ require 'cijoe/version'
28
+ require 'cijoe/config'
29
+ require 'cijoe/commit'
30
+ require 'cijoe/build'
31
+ require 'cijoe/campfire'
32
+ require 'cijoe/gmail'
33
+ require 'cijoe/server'
34
+
35
+ class CIJoe
36
+ attr_reader :user, :project, :url, :current_build, :last_build
37
+
38
+ def initialize(project_path)
39
+ project_path = File.expand_path(project_path)
40
+ Dir.chdir(project_path)
41
+
42
+ @user, @project = git_user_and_project
43
+ @url = "http://github.com/#{@user}/#{@project}"
44
+
45
+ @last_build = nil
46
+ @current_build = nil
47
+
48
+ trap("INT") { stop }
49
+ end
50
+
51
+ # is a build running?
52
+ def building?
53
+ !!@current_build
54
+ end
55
+
56
+ # the pid of the running child process
57
+ def pid
58
+ building? and @pid
59
+ end
60
+
61
+ # kill the child and exit
62
+ def stop
63
+ Process.kill(9, pid) if pid
64
+ exit!
65
+ end
66
+
67
+ # build callbacks
68
+ def build_failed(output, error)
69
+ finish_build :failed, "#{error}\n\n#{output}"
70
+ run_hook "build-failed"
71
+ end
72
+
73
+ def build_worked(output)
74
+ finish_build :worked, output
75
+ run_hook "build-worked"
76
+ end
77
+
78
+ def finish_build(status, output)
79
+ @current_build.finished_at = Time.now
80
+ @current_build.status = status
81
+ @current_build.output = output
82
+ @last_build = @current_build
83
+ @current_build = nil
84
+ @last_build.notify if @last_build.respond_to? :notify
85
+ end
86
+
87
+ # run the build but make sure only
88
+ # one is running at a time
89
+ def build
90
+ return if building?
91
+ @current_build = Build.new(@user, @project)
92
+ Thread.new { build! }
93
+ end
94
+
95
+ # update git then run the build
96
+ def build!
97
+ out, err, status = '', '', nil
98
+ git_update
99
+ @current_build.sha = git_sha
100
+
101
+ status = Open4.popen4(runner_command) do |pid, stdin, stdout, stderr|
102
+ @pid = pid
103
+ err, out = stderr.read.strip, stdout.read.strip
104
+ end
105
+
106
+ status.exitstatus.to_i == 0 ? build_worked(out) : build_failed(out, err)
107
+ rescue Object => e
108
+ build_failed('', e.to_s)
109
+ end
110
+
111
+ # shellin' out
112
+ def runner_command
113
+ runner = Config.cijoe.runner.to_s
114
+ runner == '' ? "rake -s test:units" : runner
115
+ end
116
+
117
+ def git_sha
118
+ `git rev-parse origin/#{git_branch}`.chomp
119
+ end
120
+
121
+ def git_update
122
+ `git fetch origin && git reset --hard origin/#{git_branch}`
123
+ run_hook "after-reset"
124
+ end
125
+
126
+ def git_user_and_project
127
+ Config.remote.origin.url.to_s.chomp('.git').split(':')[-1].split('/')[-2, 2]
128
+ end
129
+
130
+ def git_branch
131
+ branch = Config.cijoe.branch.to_s
132
+ branch == '' ? "master" : branch
133
+ end
134
+
135
+ # massage our repo
136
+ def run_hook(hook)
137
+ if File.exists?(file=".git/hooks/#{hook}") && File.executable?(file)
138
+ data = {
139
+ "MESSAGE" => @last_build.commit.message,
140
+ "AUTHOR" => @last_build.commit.author,
141
+ "SHA" => @last_build.commit.sha,
142
+ "OUTPUT" => @last_build.clean_output
143
+ }
144
+ env = data.collect { |k, v| %(#{k}=#{v.inspect}) }.join(" ")
145
+ `#{env} sh #{file}`
146
+ end
147
+ end
148
+ end
@@ -0,0 +1,33 @@
1
+ class CIJoe
2
+ class Build < Struct.new(:user, :project, :started_at, :finished_at, :sha, :status, :output)
3
+ def initialize(*args)
4
+ super
5
+ self.started_at = Time.now
6
+ end
7
+
8
+ def status
9
+ return super if started_at && finished_at
10
+ :building
11
+ end
12
+
13
+ def failed?
14
+ status == :failed
15
+ end
16
+
17
+ def worked?
18
+ status == :worked
19
+ end
20
+
21
+ def short_sha
22
+ sha[0,7] if sha
23
+ end
24
+
25
+ def clean_output
26
+ output.gsub(/\e\[.+?m/, '').strip
27
+ end
28
+
29
+ def commit
30
+ @commit ||= Commit.new(sha, user, project)
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,72 @@
1
+ class CIJoe
2
+ module Campfire
3
+ def self.activate
4
+ if valid_config?
5
+ require 'tinder'
6
+
7
+ CIJoe::Build.class_eval do
8
+ include CIJoe::Campfire
9
+ end
10
+
11
+ puts "Loaded Campfire notifier"
12
+ else
13
+ puts "Can't load Campfire notifier."
14
+ puts "Please add the following to your project's .git/config:"
15
+ puts "[campfire]"
16
+ puts "\tuser = your@campfire.email"
17
+ puts "\tpass = passw0rd"
18
+ puts "\tsubdomain = whatever"
19
+ puts "\troom = Awesomeness"
20
+ puts "\tssl = false"
21
+ end
22
+ end
23
+
24
+ def self.config
25
+ @config ||= {
26
+ :subdomain => Config.campfire.subdomain.to_s,
27
+ :user => Config.campfire.user.to_s,
28
+ :pass => Config.campfire.pass.to_s,
29
+ :room => Config.campfire.room.to_s,
30
+ :ssl => Config.campfire.ssl.to_s.strip == 'true'
31
+ }
32
+ end
33
+
34
+ def self.valid_config?
35
+ %w( subdomain user pass room ).all? do |key|
36
+ !config[key.intern].empty?
37
+ end
38
+ end
39
+
40
+ def notify
41
+ room.speak "#{short_message}. #{commit.url}"
42
+ room.paste full_message if failed?
43
+ room.leave
44
+ end
45
+
46
+ private
47
+ def room
48
+ @room ||= begin
49
+ config = Campfire.config
50
+ options = {}
51
+ options[:ssl] = config[:ssl] ? true : false
52
+ campfire = Tinder::Campfire.new(config[:subdomain], options)
53
+ campfire.login(config[:user], config[:pass])
54
+ campfire.find_room_by_name(config[:room])
55
+ end
56
+ end
57
+
58
+ def short_message
59
+ "Build #{short_sha} of #{project} #{worked? ? "was successful" : "failed"}"
60
+ end
61
+
62
+ def full_message
63
+ <<-EOM
64
+ Commit Message: #{commit.message}
65
+ Commit Date: #{commit.committed_at}
66
+ Commit Author: #{commit.author}
67
+
68
+ #{clean_output}
69
+ EOM
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,27 @@
1
+ class CIJoe
2
+ class Commit < Struct.new(:sha, :user, :project)
3
+ def url
4
+ "http://github.com/#{user}/#{project}/commit/#{sha}"
5
+ end
6
+
7
+ def author
8
+ raw_commit_lines[1].split(':')[-1]
9
+ end
10
+
11
+ def committed_at
12
+ raw_commit_lines[2].split(':', 2)[-1]
13
+ end
14
+
15
+ def message
16
+ raw_commit_lines[4].split(':')[-1].strip
17
+ end
18
+
19
+ def raw_commit
20
+ @raw_commit ||= `git show #{sha}`.chomp
21
+ end
22
+
23
+ def raw_commit_lines
24
+ @raw_commit_lines ||= raw_commit.split("\n")
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,24 @@
1
+ class CIJoe
2
+ class Config
3
+ def self.method_missing(command, *args)
4
+ new(command)
5
+ end
6
+
7
+ def initialize(command, parent = nil)
8
+ @command = command
9
+ @parent = parent
10
+ end
11
+
12
+ def method_missing(command, *args)
13
+ Config.new(command, self)
14
+ end
15
+
16
+ def to_s
17
+ `git config #{config_string}`.chomp
18
+ end
19
+
20
+ def config_string
21
+ @parent ? "#{@parent.config_string}.#{@command}" : @command
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,142 @@
1
+ # The CI Joe Gmail notifier is a mashup of two pieces of code.
2
+ #
3
+ # 1) This gist:
4
+ #
5
+ # http://gist.github.com/122071
6
+ #
7
+ # 2) The CI Joe Campfire notifier.
8
+ #
9
+ # The CI Joe Campfire notifier uses a valid_config? method to check that the notifier will be able to work. If so, it loads
10
+ # the Campfire module right into CI Joe, so that when CI Joe calls #notify, it's calling the #notify on the Campfire module.
11
+ # Obviously, even though Chris was very explicit about not supporting any kind of notifier except the Campfire notifier, this
12
+ # is a very, very extensible API which is very, very friendly to repurposing. So I made with the repurpose. All you need to
13
+ # support is an #activate method on the module itself and a #notify instance method. Copying the #valid_config? pattern is
14
+ # strongly advised but absolutely not required.
15
+
16
+ class CIJoe
17
+ module Gmail
18
+ def self.activate
19
+ if valid_config?
20
+ require "openssl"
21
+ require "net/smtp"
22
+
23
+ # http://www.jamesbritt.com/2007/12/18/sending-mail-through-gmail-with-ruby-s-net-smtp
24
+ # http://d.hatena.ne.jp/zorio/20060416
25
+
26
+ Net::SMTP.class_eval do
27
+ private
28
+ def do_start(helodomain, user, secret, authtype)
29
+ raise IOError, 'SMTP session already started' if @started
30
+ check_auth_args user, secret, authtype if user or secret
31
+
32
+ sock = timeout(@open_timeout) { TCPSocket.open(@address, @port) }
33
+ @socket = Net::InternetMessageIO.new(sock)
34
+ @socket.read_timeout = 60 #@read_timeout
35
+ @socket.debug_output = STDERR #@debug_output
36
+
37
+ check_response(critical { recv_response() })
38
+ do_helo(helodomain)
39
+
40
+ raise 'openssl library not installed' unless defined?(OpenSSL)
41
+ starttls
42
+ ssl = OpenSSL::SSL::SSLSocket.new(sock)
43
+ ssl.sync_close = true
44
+ ssl.connect
45
+ @socket = Net::InternetMessageIO.new(ssl)
46
+ @socket.read_timeout = 60 #@read_timeout
47
+ @socket.debug_output = STDERR #@debug_output
48
+ do_helo(helodomain)
49
+
50
+ authenticate user, secret, authtype if user
51
+ @started = true
52
+ ensure
53
+ unless @started
54
+ # authentication failed, cancel connection.
55
+ @socket.close if not @started and @socket and not @socket.closed?
56
+ @socket = nil
57
+ end
58
+ end
59
+
60
+ def do_helo(helodomain)
61
+ begin
62
+ if @esmtp
63
+ ehlo helodomain
64
+ else
65
+ helo helodomain
66
+ end
67
+ rescue Net::ProtocolError
68
+ if @esmtp
69
+ @esmtp = false
70
+ @error_occured = false
71
+ retry
72
+ end
73
+ raise
74
+ end
75
+ end
76
+
77
+ def starttls
78
+ getok('STARTTLS')
79
+ end
80
+ end
81
+
82
+ CIJoe::Build.class_eval do
83
+ include CIJoe::Gmail
84
+ end
85
+
86
+ puts "Loaded Gmail notifier"
87
+ else
88
+ puts "Can't load Gmail notifier."
89
+ puts "Please add the following to your project's .git/config:"
90
+ puts "[gmail]"
91
+ puts "\tuser = your_ci_joe@email"
92
+ puts "\tpass = passw0rd"
93
+ puts "\trecipient = developers@your-company.com"
94
+ end
95
+ end
96
+
97
+ def self.config
98
+ @config ||= {
99
+ :user => Config.gmail.user.to_s,
100
+ :pass => Config.gmail.pass.to_s
101
+ }
102
+ end
103
+
104
+ def self.valid_config?
105
+ %w( user pass recipient ).all? do |key|
106
+ !config[key.intern].empty?
107
+ end
108
+ end
109
+
110
+ def notify
111
+ send_email(@config[:user],
112
+ "Vidli Continuous Integration",
113
+ @config[:recipient],
114
+ @config[:recipient],
115
+ short_message,
116
+ "#{full_message}\n#{commit.url}")
117
+ end
118
+
119
+ private
120
+ # http://snippets.dzone.com/posts/show/2362
121
+ def send_email(from, from_alias, to, to_alias, subject, message)
122
+ msg = <<END_OF_MESSAGE
123
+ From: #{from_alias} <#{from}>
124
+ To: #{to_alias} <#{to}>
125
+ Subject: #{subject}
126
+
127
+ #{message}
128
+ END_OF_MESSAGE
129
+
130
+ Net::SMTP.start("smtp.gmail.com",
131
+ 587,
132
+ "localhost.localdomain",
133
+ @config[:user],
134
+ @config[:pass],
135
+ "plain") do |smtp|
136
+ smtp.send_message msg, from, to
137
+ end
138
+ end
139
+
140
+ end
141
+ end
142
+
@@ -0,0 +1,212 @@
1
+ /*****************************************************************************/
2
+ /*
3
+ /* Common
4
+ /*
5
+ /*****************************************************************************/
6
+
7
+ /* Global Reset */
8
+
9
+ * {
10
+ margin: 0;
11
+ padding: 0;
12
+ }
13
+
14
+ html, body {
15
+ height: 100%;
16
+ }
17
+
18
+ body {
19
+ background-color: white;
20
+ font: 13.34px helvetica, arial, clean, sans-serif;
21
+ *font-size: small;
22
+ text-align: center;
23
+ }
24
+
25
+ h1, h2, h3, h4, h5, h6 {
26
+ font-size: 100%;
27
+ }
28
+
29
+ h1 {
30
+ margin-bottom: 1em;
31
+ }
32
+
33
+ h1 a {
34
+ text-decoration: none;
35
+ color: #000;
36
+ }
37
+
38
+ .failed, .color31 {
39
+ color: red !important;
40
+ }
41
+
42
+ .worked, .color32 {
43
+ color: green !important;
44
+ }
45
+
46
+ .errored, .color33 {
47
+ color: yellow !important;
48
+ }
49
+
50
+ p {
51
+ margin: 1em 0;
52
+ }
53
+
54
+ a {
55
+ color: #00a;
56
+ }
57
+
58
+ a:hover {
59
+ color: black;
60
+ }
61
+
62
+ a:visited {
63
+ color: #a0a;
64
+ }
65
+
66
+ table {
67
+ font-size: inherit;
68
+ font: 100%;
69
+ }
70
+
71
+ /*****************************************************************************/
72
+ /*
73
+ /* Home
74
+ /*
75
+ /*****************************************************************************/
76
+
77
+ ul.posts {
78
+ list-style-type: none;
79
+ margin-bottom: 2em;
80
+ }
81
+
82
+ ul.posts li {
83
+ line-height: 1.75em;
84
+ }
85
+
86
+ ul.posts .date {
87
+ color: #aaa;
88
+ font-family: Monaco, "Courier New", monospace;
89
+ font-size: 80%;
90
+ }
91
+
92
+ /*****************************************************************************/
93
+ /*
94
+ /* Site
95
+ /*
96
+ /*****************************************************************************/
97
+
98
+ .site {
99
+ font-size: 110%;
100
+ text-align: justify;
101
+ width: 40em;
102
+ margin: 3em auto 2em auto;
103
+ line-height: 1.5em;
104
+ }
105
+
106
+ .title {
107
+ color: #a00;
108
+ font-weight: bold;
109
+ margin-bottom: 2em;
110
+ }
111
+
112
+ .site .title a {
113
+ color: #a00;
114
+ text-decoration: none;
115
+ }
116
+
117
+ .site .title a:hover {
118
+ color: black;
119
+ }
120
+
121
+ .site .title .extra {
122
+ color: #aaa;
123
+ text-decoration: none;
124
+ margin-left: 1em;
125
+ font-size: 0.9em;
126
+ }
127
+
128
+ .site .title a.extra:hover {
129
+ color: black;
130
+ }
131
+
132
+ .site .meta {
133
+ color: #aaa;
134
+ }
135
+
136
+ .site .footer {
137
+ font-size: 80%;
138
+ color: #666;
139
+ border-top: 4px solid #eee;
140
+ margin-top: 2em;
141
+ overflow: hidden;
142
+ }
143
+
144
+ .site .footer .contact {
145
+ float: left;
146
+ margin-right: 3em;
147
+ }
148
+
149
+ .site .footer .contact a {
150
+ color: #8085C1;
151
+ }
152
+
153
+ .site .footer .rss {
154
+ margin-top: 1.1em;
155
+ margin-right: -.2em;
156
+ float: right;
157
+ }
158
+
159
+ .site .footer .rss img {
160
+ border: 0;
161
+ }
162
+
163
+ /*****************************************************************************/
164
+ /*
165
+ /* Posts
166
+ /*
167
+ /*****************************************************************************/
168
+
169
+ #post {
170
+
171
+ }
172
+
173
+ /* standard */
174
+
175
+ #post pre {
176
+ border: 1px solid #ddd;
177
+ background-color: #eef;
178
+ padding: 0 .4em;
179
+ }
180
+
181
+ #post ul,
182
+ #post ol {
183
+ margin-left: 1.25em;
184
+ }
185
+
186
+ #post code {
187
+ border: 1px solid #ddd;
188
+ background-color: #eef;
189
+ font-size: 95%;
190
+ padding: 0 .2em;
191
+ }
192
+
193
+ #post pre code {
194
+ border: none;
195
+ }
196
+
197
+ /* terminal */
198
+
199
+ pre.terminal {
200
+ border: 1px solid black;
201
+ background-color: #333;
202
+ color: white;
203
+ padding: 5px;
204
+ overflow: auto;
205
+ word-wrap: break-word;
206
+ }
207
+
208
+ pre.terminal code {
209
+ font-family: 'Bitstream Vera Sans Mono', 'Courier', monospace;
210
+ background-color: #333;
211
+ }
212
+
@@ -0,0 +1,84 @@
1
+ require 'sinatra/base'
2
+ require 'erb'
3
+
4
+ class CIJoe
5
+ class Server < Sinatra::Base
6
+ dir = File.dirname(File.expand_path(__FILE__))
7
+
8
+ set :views, "#{dir}/views"
9
+ set :public, "#{dir}/public"
10
+ set :static, true
11
+
12
+ get '/?' do
13
+ erb(:template, {}, :joe => @joe)
14
+ end
15
+
16
+ post '/?' do
17
+ payload = params[:payload].to_s
18
+ if payload.empty? || payload.include?(@joe.git_branch)
19
+ @joe.build
20
+ end
21
+ redirect request.path
22
+ end
23
+
24
+ user, pass = Config.cijoe.user.to_s, Config.cijoe.pass.to_s
25
+ if user != '' && pass != ''
26
+ use Rack::Auth::Basic do |username, password|
27
+ [ username, password ] == [ user, pass ]
28
+ end
29
+ puts "Using HTTP basic auth"
30
+ end
31
+
32
+ helpers do
33
+ include Rack::Utils
34
+ alias_method :h, :escape_html
35
+
36
+ # thanks integrity!
37
+ def ansi_color_codes(string)
38
+ string.gsub("\e[0m", '</span>').
39
+ gsub(/\e\[(\d+)m/, "<span class=\"color\\1\">")
40
+ end
41
+
42
+ def pretty_time(time)
43
+ time.strftime("%Y-%m-%d %H:%M")
44
+ end
45
+
46
+ def cijoe_root
47
+ root = request.path
48
+ root = "" if root == "/"
49
+ root
50
+ end
51
+ end
52
+
53
+ def initialize(*args)
54
+ super
55
+ check_project
56
+ @joe = CIJoe.new(options.project_path)
57
+
58
+ # example .gitconfig:
59
+ # [cijoe]
60
+ # notifier = CIJoe::Campfire
61
+ #
62
+ # or
63
+ # [cijoe]
64
+ # notifier = CIJoe::Gmail
65
+ #
66
+ # this also gives you the option of running CI Joe without any notifier at all.
67
+
68
+ eval(Config.cijoe.notifier.to_s).activate unless Config.cijoe.notifier.nil?
69
+ end
70
+
71
+ def self.start(host, port, project_path)
72
+ set :project_path, project_path
73
+ CIJoe::Server.run! :host => host, :port => port
74
+ end
75
+
76
+ def check_project
77
+ if options.project_path.nil? || !File.exists?(File.expand_path(options.project_path))
78
+ puts "Whoops! I need the path to a Git repo."
79
+ puts " $ git clone git@github.com:username/project.git project"
80
+ abort " $ cijoe project"
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,3 @@
1
+ class CIJoe
2
+ Version = "0.1.3"
3
+ end
@@ -0,0 +1,64 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <link href="<%= cijoe_root %>/screen.css" media="screen" rel="stylesheet" type="text/css" />
5
+ <title><%= h(joe.project) %>: CI Joe</title>
6
+ </head>
7
+ <body>
8
+ <div class="site">
9
+ <div class="title">
10
+ <a href="<%= cijoe_root %>/">CI Joe</a>
11
+ <span class="extra">because knowing is half the battle</span>
12
+ </div>
13
+
14
+ <div id="home">
15
+ <h1><a href="<%= joe.url %>"><%= joe.project %></a></h1>
16
+ <ul class="posts">
17
+ <% if joe.current_build %>
18
+ <li>
19
+ <span class="date"><%= pretty_time(joe.current_build.started_at) if joe.current_build %></span> &raquo;
20
+ <% if joe.current_build.sha %>
21
+ Building <a href="<%= joe.url %>/commits/<%= joe.current_build.sha %>"><%= joe.current_build.short_sha %></a> <small>(pid: <%= joe.pid %>)</small>
22
+ <% else %>
23
+ Build starting...
24
+ <% end %>
25
+ </li>
26
+ <% else %>
27
+ <li><form method="POST"><input type="submit" value="Build"/></form></li>
28
+ <% end %>
29
+
30
+ <% if joe.last_build %>
31
+ <li><span class="date"><%= pretty_time(joe.last_build.finished_at) %></span> &raquo; Built <a href="<%= joe.url %>/commits/<%= joe.last_build.sha %>"><%= joe.last_build.short_sha %></a> <span class="<%= joe.last_build.status %>">(<%= joe.last_build.status %>)</span></li>
32
+ <% if joe.last_build.failed? %>
33
+ <li><pre class="terminal"><code><%=ansi_color_codes h(joe.last_build.output) %></code></pre></li>
34
+ <% end %>
35
+ <% end %>
36
+ </ul>
37
+ </div>
38
+
39
+ <div class="footer">
40
+ <div class="contact">
41
+ <p>
42
+ <a href="http://github.com/defunkt/cijoe/tree/master#readme">Documentation</a><br/>
43
+ <a href="http://github.com/defunkt/cijoe">Source</a><br/>
44
+ <a href="http://github.com/defunkt/cijoe/issues">Issues</a><br/>
45
+ <a href="http://twitter.com/defunkt">Twitter</a>
46
+ </p>
47
+ </div>
48
+ <div class="contact">
49
+ <p>
50
+ Designed by <a href="http://tom.preston-werner.com/">Tom Preston-Werner</a><br/>
51
+ Influenced by <a href="http://integrityapp.com/">Integrity</a><br/>
52
+ Built with <a href="http://sinatrarb.com/">Sinatra</a><br/>
53
+ Keep it simple, Sam.
54
+ </p>
55
+ </div>
56
+ <div class="rss">
57
+ <a href="http://github.com/defunkt/cijoe">
58
+ <img src="<%= cijoe_root %>/octocat.png" alt="Octocat!" />
59
+ </a>
60
+ </div>
61
+ </div>
62
+ </div>
63
+ </body>
64
+ </html>
metadata ADDED
@@ -0,0 +1,103 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: snake_eyes
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.3
5
+ platform: ruby
6
+ authors:
7
+ - Chris Wanstrath
8
+ - Giles Bowkett
9
+ - Dr. McNinja
10
+ autorequire:
11
+ bindir: bin
12
+ cert_chain: []
13
+
14
+ date: 2009-10-29 00:00:00 -07:00
15
+ default_executable: snake_eyes
16
+ dependencies:
17
+ - !ruby/object:Gem::Dependency
18
+ name: choice
19
+ type: :runtime
20
+ version_requirement:
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: "0"
26
+ version:
27
+ - !ruby/object:Gem::Dependency
28
+ name: sinatra
29
+ type: :runtime
30
+ version_requirement:
31
+ version_requirements: !ruby/object:Gem::Requirement
32
+ requirements:
33
+ - - ">="
34
+ - !ruby/object:Gem::Version
35
+ version: "0"
36
+ version:
37
+ - !ruby/object:Gem::Dependency
38
+ name: open4
39
+ type: :runtime
40
+ version_requirement:
41
+ version_requirements: !ruby/object:Gem::Requirement
42
+ requirements:
43
+ - - ">="
44
+ - !ruby/object:Gem::Version
45
+ version: "0"
46
+ version:
47
+ description: Snake Eyes is a fork of CI Joe, a simple Continuous Integration server.
48
+ email: gilesb@gmail.com
49
+ executables:
50
+ - snake_eyes
51
+ extensions: []
52
+
53
+ extra_rdoc_files:
54
+ - LICENSE
55
+ - README.markdown
56
+ files:
57
+ - LICENSE
58
+ - README.markdown
59
+ - Rakefile
60
+ - bin/snake_eyes
61
+ - deps.rip
62
+ - examples/cijoed
63
+ - lib/cijoe.rb
64
+ - lib/cijoe/build.rb
65
+ - lib/cijoe/campfire.rb
66
+ - lib/cijoe/commit.rb
67
+ - lib/cijoe/config.rb
68
+ - lib/cijoe/gmail.rb
69
+ - lib/cijoe/public/octocat.png
70
+ - lib/cijoe/public/screen.css
71
+ - lib/cijoe/server.rb
72
+ - lib/cijoe/version.rb
73
+ - lib/cijoe/views/template.erb
74
+ has_rdoc: true
75
+ homepage: http://github.com/gilesbowkett/snake_eyes
76
+ licenses: []
77
+
78
+ post_install_message:
79
+ rdoc_options:
80
+ - --charset=UTF-8
81
+ require_paths:
82
+ - lib
83
+ required_ruby_version: !ruby/object:Gem::Requirement
84
+ requirements:
85
+ - - ">="
86
+ - !ruby/object:Gem::Version
87
+ version: "0"
88
+ version:
89
+ required_rubygems_version: !ruby/object:Gem::Requirement
90
+ requirements:
91
+ - - ">="
92
+ - !ruby/object:Gem::Version
93
+ version: "0"
94
+ version:
95
+ requirements: []
96
+
97
+ rubyforge_project:
98
+ rubygems_version: 1.3.5
99
+ signing_key:
100
+ specification_version: 3
101
+ summary: Snake Eyes is a fork of CI Joe, a simple Continuous Integration server.
102
+ test_files: []
103
+