snake_eyes 0.1.3

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