mheffner-cijoe 0.2.0

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.
@@ -0,0 +1,6 @@
1
+ .DS_Store
2
+ rdoc
3
+ pkg
4
+ *.tmproj
5
+ tmp/**/*
6
+ tmp/*
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,159 @@
1
+ CI Joe
2
+ ======
3
+
4
+ Joe is a [Continuous
5
+ Integration](http://en.wikipedia.org/wiki/Continuous_integration)
6
+ server that'll run your tests on demand and report their pass/fail status.
7
+
8
+ Because knowing is half the battle.
9
+
10
+ ![The Battle](http://img.skitch.com/20090805-g4a2qhttwij8n2jr9t552efn3k.png)
11
+
12
+ Quickstart
13
+ ----------
14
+
15
+ Rip:
16
+
17
+ $ rip install git://github.com/defunkt/cijoe.git
18
+ $ git clone git://github.com/you/yourrepo.git
19
+ $ cijoe yourrepo
20
+
21
+ Gemcutter:
22
+
23
+ $ gem install cijoe
24
+ $ git clone git://github.com/you/yourrepo.git
25
+ $ cijoe yourrepo
26
+
27
+ Boom. Navigate to http://localhost:4567 to see Joe in action.
28
+ Check `cijoe -h` for other options.
29
+
30
+ Basically you need to run `cijoe` and hand it the path to a git
31
+ repo. Make sure this isn't a shared repo: Joe needs to own it.
32
+
33
+ Joe looks for various git config settings in the repo you hand it. For
34
+ instance, you can tell Joe what command to run by setting
35
+ `cijoe.runner`:
36
+
37
+ $ git config --add cijoe.runner "rake -s test:units"
38
+
39
+ Joe doesn't care about Ruby, Python, or whatever. As long as the
40
+ runner returns a non-zero exit status on fail and a zero on success,
41
+ everyone is happy.
42
+
43
+ Need to do some massaging of your repo before the tests run, like
44
+ maybe swapping in a new database.yml? No problem - Joe will try to
45
+ run `.git/hooks/after-reset` if it exists before each build phase.
46
+ Do it in there. Just make sure it's executable.
47
+
48
+ Want to notify IRC or email on test pass or failure? Joe will run
49
+ `.git/hooks/build-failed` or `.git/hooks/build-worked` if they exist
50
+ and are executable on build pass / fail. They're just shell scripts -
51
+ put whatever you want in there.
52
+
53
+ Tip: your repo's `HEAD` will point to the commit used to run the
54
+ build. Pull any metadata you want out of that scro.
55
+
56
+
57
+ Other Branches
58
+ --------------
59
+
60
+ Want joe to run against a branch other than `master`? No problem:
61
+
62
+ $ git config --add cijoe.branch deploy
63
+
64
+
65
+ Campfire
66
+ --------
67
+
68
+ Campfire notification is included, because it's what we use. Want Joe
69
+ notify your Campfire? Put this in your repo's `.git/config`:
70
+
71
+ [campfire]
72
+ user = your@campfire.email
73
+ pass = passw0rd
74
+ subdomain = whatever
75
+ room = Awesomeness
76
+ ssl = false
77
+
78
+ Or do it the old fashion way:
79
+
80
+ $ cd yourrepo
81
+ $ git config --add campfire.user chris@ozmm.org
82
+ $ git config --add campfire.subdomain github
83
+ etc.
84
+
85
+
86
+ Checkin' Status
87
+ ---------------
88
+
89
+ Want to see how your build's doing without any of this fancy UI crap?
90
+ Ping Joe for the lowdown:
91
+
92
+ curl http://localhost:4567/ping
93
+
94
+ Joe will return `200 OK` if all is quiet on the Western Front. If
95
+ Joe's busy building or your last build failed, you'll get `412
96
+ PRECONDITION FAILED`.
97
+
98
+ Multiple Projects
99
+ -----------------
100
+
101
+ Want CI for multiple projects? Just start multiple instances of Joe!
102
+ He can run on any port - try `cijoe -h` for more options.
103
+
104
+
105
+ HTTP Auth
106
+ ---------
107
+
108
+ Worried about people triggering your builds? Setup HTTP auth:
109
+
110
+ $ git config --add cijoe.user chris
111
+ $ git config --add cijoe.pass secret
112
+
113
+
114
+ GitHub Integration
115
+ ------------------
116
+
117
+ Any POST to Joe will trigger a build. If you are hiding Joe behind
118
+ HTTP auth, that's okay - GitHub knows how to authenticate properly.
119
+
120
+ ![Post-Receive URL](http://img.skitch.com/20090806-d2bxrk733gqu8m11tf4kyir5d8.png)
121
+
122
+ You can find the Post-Receive option under the 'Service Hooks' subtab
123
+ of your project's "Admin" tab.
124
+
125
+
126
+ Daemonize
127
+ ---------
128
+
129
+ Want to run Joe as a daemon? Use `nohup`:
130
+
131
+ $ nohup cijoe -p 4444 repo &
132
+
133
+
134
+ Other CI Servers
135
+ ----------------
136
+
137
+ Need more features? More notifiers? Check out one of these bad boys:
138
+
139
+ * [Cerberus](http://cerberus.rubyforge.org/)
140
+ * [Integrity](http://integrityapp.com/)
141
+ * [CruiseControl.rb](http://cruisecontrolrb.thoughtworks.com/)
142
+ * [BuildBot](http://buildbot.net/trac)
143
+
144
+
145
+ Screenshots
146
+ -----------
147
+
148
+ ![Building](http://img.skitch.com/20090806-ryw34ksi5ixnrdwxcptqy28iy7.png)
149
+
150
+ ![Built](http://img.skitch.com/20090806-f7j3r65yecaq13hdcxqwtc5krd.)
151
+
152
+
153
+ Questions? Concerns?
154
+ --------------------
155
+
156
+ [Issues](http://github.com/defunkt/cijoe/issues) or [the mailing list](http://groups.google.com/group/cijoe).
157
+
158
+
159
+ ( Chris Wanstrath :: chris@ozmm.org )
@@ -0,0 +1,59 @@
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 = "mheffner-cijoe"
12
+ gemspec.summary = "CI Joe is a simple Continuous Integration server."
13
+ gemspec.description = "CI Joe is a simple Continuous Integration server."
14
+ gemspec.email = "chris@ozmm.org"
15
+ gemspec.homepage = "http://github.com/defunkt/cijoe"
16
+ gemspec.authors = ["Chris Wanstrath"]
17
+ gemspec.add_dependency 'choice'
18
+ gemspec.add_dependency 'sinatra'
19
+ gemspec.add_development_dependency 'rack-test'
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
26
+
27
+ require 'rake/testtask'
28
+ Rake::TestTask.new(:test) do |test|
29
+ test.libs << 'lib' << 'test'
30
+ test.pattern = 'test/**/test_*.rb'
31
+ test.verbose = true
32
+ end
33
+
34
+ begin
35
+ require 'rcov/rcovtask'
36
+ Rcov::RcovTask.new do |test|
37
+ test.libs << 'test'
38
+ test.pattern = 'test/**/test_*.rb'
39
+ test.verbose = true
40
+ end
41
+ rescue LoadError
42
+ task :rcov do
43
+ abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
44
+ end
45
+ end
46
+
47
+ task :test => :check_dependencies
48
+
49
+ task :default => :test
50
+
51
+ require 'rake/rdoctask'
52
+ Rake::RDocTask.new do |rdoc|
53
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
54
+
55
+ rdoc.rdoc_dir = 'rdoc'
56
+ rdoc.title = "someproject #{version}"
57
+ rdoc.rdoc_files.include('README*')
58
+ rdoc.rdoc_files.include('lib/**/*.rb')
59
+ 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,4 @@
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
@@ -0,0 +1,15 @@
1
+ # Example CI Joe rackup config. Drop a cijoe.ru file
2
+ # in your projects direct
3
+ require 'cijoe'
4
+
5
+ # setup middleware
6
+ use Rack::CommonLogger
7
+
8
+ # configure joe
9
+ CIJoe::Server.configure do |config|
10
+ config.set :project_path, File.dirname(__FILE__)
11
+ config.set :show_exceptions, true
12
+ config.set :lock, true
13
+ end
14
+
15
+ run CIJoe::Server
@@ -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,202 @@
1
+ ##
2
+ # CI Joe.
3
+ # Because knowing is half the battle.
4
+ #
5
+ # This is a stupid simple CI server. It can build one (1)
6
+ # git-based project only.
7
+ #
8
+ # It only remembers the last build.
9
+ #
10
+ # It only notifies to Campfire.
11
+ #
12
+ # It's a RAH (Real American Hero).
13
+ #
14
+ # Seriously, I'm gonna be nuts about keeping this simple.
15
+
16
+ require 'cijoe/version'
17
+ require 'cijoe/config'
18
+ require 'cijoe/commit'
19
+ require 'cijoe/build'
20
+ require 'cijoe/campfire'
21
+ require 'cijoe/server'
22
+
23
+ class CIJoe
24
+ attr_reader :user, :project, :url, :current_build, :last_build
25
+
26
+ def initialize(project_path)
27
+ project_path = File.expand_path(project_path)
28
+ Dir.chdir(project_path)
29
+
30
+ @user, @project = git_user_and_project
31
+ @url = "http://github.com/#{@user}/#{@project}"
32
+
33
+ @last_build = nil
34
+ @current_build = nil
35
+
36
+ trap("INT") { stop }
37
+ end
38
+
39
+ # is a build running?
40
+ def building?
41
+ !!@current_build
42
+ end
43
+
44
+ # the pid of the running child process
45
+ def pid
46
+ building? and current_build.pid
47
+ end
48
+
49
+ # kill the child and exit
50
+ def stop
51
+ Process.kill(9, pid) if pid
52
+ exit!
53
+ end
54
+
55
+ # build callbacks
56
+ def build_failed(output, error)
57
+ finish_build :failed, "#{error}\n\n#{output}"
58
+ run_hook "build-failed"
59
+ end
60
+
61
+ def build_worked(output)
62
+ finish_build :worked, output
63
+ run_hook "build-worked"
64
+ end
65
+
66
+ def finish_build(status, output)
67
+ @current_build.finished_at = Time.now
68
+ @current_build.status = status
69
+ @current_build.output = output
70
+ @last_build = @current_build
71
+
72
+ @current_build = nil
73
+ write_build 'current', @current_build
74
+ write_build 'last', @last_build
75
+ @last_build.notify if @last_build.respond_to? :notify
76
+ end
77
+
78
+ # run the build but make sure only
79
+ # one is running at a time
80
+ def build
81
+ return if building?
82
+ @current_build = Build.new(@user, @project)
83
+ write_build 'current', @current_build
84
+ Thread.new { build! }
85
+ end
86
+
87
+ def open_pipe(cmd)
88
+ read, write = IO.pipe
89
+
90
+ pid = fork do
91
+ read.close
92
+ $stdout.reopen write
93
+ exec cmd
94
+ end
95
+
96
+ write.close
97
+
98
+ yield read, pid
99
+ end
100
+
101
+ # update git then run the build
102
+ def build!
103
+ build = @current_build
104
+ output = ''
105
+ git_update
106
+ build.sha = git_sha
107
+ write_build 'current', build
108
+
109
+ open_pipe("#{runner_command} 2>&1") do |pipe, pid|
110
+ puts "#{Time.now.to_i}: Building #{build.short_sha}: pid=#{pid}"
111
+
112
+ build.pid = pid
113
+ write_build 'current', build
114
+ output = pipe.read
115
+ end
116
+
117
+ Process.waitpid(build.pid)
118
+ status = $?.exitstatus.to_i
119
+ puts "#{Time.now.to_i}: Built #{build.short_sha}: status=#{status}"
120
+
121
+ status == 0 ? build_worked(output) : build_failed('', output)
122
+ rescue Object => e
123
+ puts "Exception building: #{e.message} (#{e.class})"
124
+ build_failed('', e.to_s)
125
+ end
126
+
127
+ # shellin' out
128
+ def runner_command
129
+ runner = Config.cijoe.runner.to_s
130
+ runner == '' ? "rake -s test:units" : runner
131
+ end
132
+
133
+ def git_sha
134
+ `git rev-parse origin/#{git_branch}`.chomp
135
+ end
136
+
137
+ def git_update
138
+ `git fetch origin && git reset --hard origin/#{git_branch}`
139
+ run_hook "after-reset"
140
+ end
141
+
142
+ def git_user_and_project
143
+ Config.remote.origin.url.to_s.chomp('.git').split(':')[-1].split('/')[-2, 2]
144
+ end
145
+
146
+ def git_branch
147
+ branch = Config.cijoe.branch.to_s
148
+ branch == '' ? "master" : branch
149
+ end
150
+
151
+ # massage our repo
152
+ def run_hook(hook)
153
+ if File.exists?(file=".git/hooks/#{hook}") && File.executable?(file)
154
+ data =
155
+ if @last_build && @last_build.commit
156
+ {
157
+ "MESSAGE" => @last_build.commit.message,
158
+ "AUTHOR" => @last_build.commit.author,
159
+ "SHA" => @last_build.commit.sha,
160
+ "OUTPUT" => @last_build.clean_output
161
+ }
162
+ else
163
+ {}
164
+ end
165
+ env = data.collect { |k, v| %(#{k}=#{v.inspect}) }.join(" ")
166
+ `#{env} sh #{file}`
167
+ end
168
+ end
169
+
170
+ # restore current / last build state from disk.
171
+ def restore
172
+ unless @last_build
173
+ @last_build = read_build('last')
174
+ end
175
+
176
+ unless @current_build
177
+ @current_build = read_build('current')
178
+ end
179
+
180
+ Process.kill(0, @current_build.pid) if @current_build && @current_build.pid
181
+ rescue Errno::ESRCH
182
+ # build pid isn't running anymore. assume previous
183
+ # server died and reset.
184
+ @current_build = nil
185
+ end
186
+
187
+ # write build info for build to file.
188
+ def write_build(name, build)
189
+ filename = ".git/builds/#{name}"
190
+ Dir.mkdir '.git/builds' unless File.directory?('.git/builds')
191
+ if build
192
+ build.dump filename
193
+ elsif File.exist?(filename)
194
+ File.unlink filename
195
+ end
196
+ end
197
+
198
+ # load build info from file.
199
+ def read_build(name)
200
+ Build.load(".git/builds/#{name}")
201
+ end
202
+ end