felixclack-cijoe 0.9.1

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,175 @@
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)](http://nerduo.com/thebattle/)
11
+
12
+ ([Buy the shirt](http://nerduo.com/thebattle/))
13
+
14
+ Quickstart
15
+ ----------
16
+
17
+ RubyGems:
18
+
19
+ $ gem install cijoe
20
+ $ git clone git://github.com/you/yourrepo.git
21
+ $ cijoe yourrepo
22
+
23
+ Boom. Navigate to <http://localhost:4567> to see Joe in action.
24
+ Check `cijoe -h` for other options.
25
+
26
+ Basically you need to run `cijoe` and hand it the path to a git
27
+ repo. Make sure this isn't a shared repo: Joe needs to own it.
28
+
29
+ Joe looks for various git config settings in the repo you hand it. For
30
+ instance, you can tell Joe what command to run by setting
31
+ `cijoe.runner`:
32
+
33
+ $ git config --add cijoe.runner "rake -s test:units"
34
+
35
+ Joe doesn't care about Ruby, Python, or whatever. As long as the
36
+ runner returns a non-zero exit status on fail and a zero on success,
37
+ everyone is happy.
38
+
39
+ Need to do some massaging of your repo before the tests run, like
40
+ maybe swapping in a new database.yml? No problem - Joe will try to
41
+ run `.git/hooks/after-reset` if it exists before each build phase.
42
+ Do it in there. Just make sure it's executable.
43
+
44
+ Want to notify IRC or email on test pass or failure? Joe will run
45
+ `.git/hooks/build-failed` or `.git/hooks/build-worked` if they exist
46
+ and are executable on build pass / fail. They're just shell scripts -
47
+ put whatever you want in there.
48
+
49
+ Tip: your repo's `HEAD` will point to the commit used to run the
50
+ build. Pull any metadata you want out of that scro.
51
+
52
+
53
+ Other Branches
54
+ ----------------------
55
+
56
+ Want joe to run against a branch other than `master`? No problem:
57
+
58
+ $ git config --add cijoe.branch deploy
59
+
60
+
61
+ Concurrent Push's - a kind of "queueing"
62
+ ----------------------------------------
63
+
64
+ Joe runs just one build at the time. If you expect concurrent push's
65
+ to your repo and want joe to build each in a kind of queue, just set:
66
+
67
+ $ git config --add cijoe.buildallfile tmp/cijoe.txt
68
+
69
+ Joe will save requests while another build runs. If more than one push
70
+ hits joe, he just picks the last after finishing the prior.
71
+
72
+
73
+ Campfire
74
+ -------------
75
+
76
+ Campfire notification is included, because it's what we use. Want Joe
77
+ notify your Campfire? Put this in 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 fashion 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
+
94
+ Checkin' Status
95
+ ----------------------
96
+
97
+ Want to see how your build's doing without any of this fancy UI crap?
98
+ Ping Joe for the lowdown:
99
+
100
+ curl http://localhost:4567/ping
101
+
102
+ Joe will return `200 OK` if all is quiet on the Western Front. If
103
+ Joe's busy building or your last build failed, you'll get `412
104
+ PRECONDITION FAILED`.
105
+
106
+
107
+ Multiple Projects
108
+ ------------------------
109
+
110
+ Want CI for multiple projects? Just start multiple instances of Joe!
111
+ He can run on any port - try `cijoe -h` for more options.
112
+
113
+ If you're using Passenger, see [this blog post](http://chrismdp.github.com/2010/03/multiple-ci-joes-with-rack-and-passenger/).
114
+
115
+
116
+ HTTP Auth
117
+ ----------------
118
+
119
+ Worried about people triggering your builds? Setup HTTP auth:
120
+
121
+ $ git config --add cijoe.user chris
122
+ $ git config --add cijoe.pass secret
123
+
124
+
125
+ GitHub Integration
126
+ --------------------------
127
+
128
+ Any POST to Joe will trigger a build. If you are hiding Joe behind
129
+ HTTP auth, that's okay - GitHub knows how to authenticate properly.
130
+
131
+ ![Post-Receive URL](http://img.skitch.com/20090806-d2bxrk733gqu8m11tf4kyir5d8.png)
132
+
133
+ You can find the Post-Receive option under the 'Service Hooks' subtab
134
+ of your project's "Admin" tab.
135
+
136
+
137
+ Daemonize
138
+ ----------------
139
+
140
+ Want to run Joe as a daemon? Use `nohup`:
141
+
142
+ $ nohup cijoe -p 4444 repo &
143
+
144
+
145
+ Other CI Servers
146
+ ------------------------
147
+
148
+ Need more features? More notifiers? Check out one of these bad boys:
149
+
150
+ * [Jenkins](http://jenkins-ci.org/)
151
+ * [Cerberus](http://cerberus.rubyforge.org/)
152
+ * [Integrity](http://integrityapp.com/)
153
+ * [CruiseControl.rb](http://cruisecontrolrb.thoughtworks.com/)
154
+ * [BuildBot](http://buildbot.net/trac)
155
+ * [Signal](http://www.github.com/dcrec1/signal)
156
+
157
+
158
+ Does GitHub use cijoe?
159
+ ---------------------------------
160
+
161
+ No. We use [Jenkins](http://jenkins-ci.org/).
162
+
163
+
164
+ Screenshots
165
+ ------------------
166
+
167
+ ![Building](http://img.skitch.com/20090806-ryw34ksi5ixnrdwxcptqy28iy7.png)
168
+
169
+ ![Built](http://img.skitch.com/20090806-f7j3r65yecaq13hdcxqwtc5krd.)
170
+
171
+
172
+ Questions? Concerns?
173
+ ---------------------------------
174
+
175
+ [Issues](http://github.com/defunkt/cijoe/issues)
@@ -0,0 +1,31 @@
1
+ require 'rake/testtask'
2
+ Rake::TestTask.new(:test) do |test|
3
+ test.libs << 'lib' << 'test'
4
+ test.pattern = 'test/**/test_*.rb'
5
+ test.verbose = true
6
+ end
7
+
8
+ begin
9
+ require 'rcov/rcovtask'
10
+ Rcov::RcovTask.new do |test|
11
+ test.libs << 'test'
12
+ test.pattern = 'test/**/test_*.rb'
13
+ test.verbose = true
14
+ end
15
+ rescue LoadError
16
+ task :rcov do
17
+ abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
18
+ end
19
+ end
20
+
21
+ task :default => :test
22
+
23
+ require 'rake/rdoctask'
24
+ Rake::RDocTask.new do |rdoc|
25
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
26
+
27
+ rdoc.rdoc_dir = 'rdoc'
28
+ rdoc.title = "someproject #{version}"
29
+ rdoc.rdoc_files.include('README*')
30
+ rdoc.rdoc_files.include('lib/**/*.rb')
31
+ end
@@ -0,0 +1,50 @@
1
+ #!/usr/bin/env ruby
2
+ $LOAD_PATH.unshift File.dirname(__FILE__) + '/../lib'
3
+
4
+ require 'choice'
5
+ require 'cijoe'
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
+
50
+ CIJoe::Server.start(options[:host], options[:port], File.expand_path(Choice.rest[0].to_s))
@@ -0,0 +1,233 @@
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
+
29
+ @user, @project = git_user_and_project
30
+ @url = "http://github.com/#{@user}/#{@project}"
31
+
32
+ @last_build = nil
33
+ @current_build = nil
34
+
35
+ trap("INT") { stop }
36
+ end
37
+
38
+ # is a build running?
39
+ def building?
40
+ !!@current_build
41
+ end
42
+
43
+ # the pid of the running child process
44
+ def pid
45
+ building? and current_build.pid
46
+ end
47
+
48
+ # kill the child and exit
49
+ def stop
50
+ # another build waits
51
+ if !repo_config.buildallfile.to_s.empty? && File.exist?(repo_config.buildallfile.to_s)
52
+ # clean out on stop
53
+ FileUtils.rm(repo_config.buildallfile.to_s)
54
+ end
55
+
56
+ Process.kill(9, pid) if pid
57
+ exit!
58
+ end
59
+
60
+ # build callbacks
61
+ def build_failed(output, error)
62
+ finish_build :failed, "#{error}\n\n#{output}"
63
+ run_hook "build-failed"
64
+ end
65
+
66
+ def build_worked(output)
67
+ finish_build :worked, output
68
+ run_hook "build-worked"
69
+ end
70
+
71
+ def finish_build(status, output)
72
+ @current_build.finished_at = Time.now
73
+ @current_build.status = status
74
+ @current_build.output = output
75
+ @last_build = @current_build
76
+
77
+ @current_build = nil
78
+ write_build 'current', @current_build
79
+ write_build 'last', @last_build
80
+ @last_build.notify if @last_build.respond_to? :notify
81
+
82
+ # another build waits
83
+ if !repo_config.buildallfile.to_s.empty? && File.exist?(repo_config.buildallfile.to_s)
84
+ # clean out before new build
85
+ FileUtils.rm(repo_config.buildallfile.to_s)
86
+ build
87
+ end
88
+ end
89
+
90
+ # run the build but make sure only one is running
91
+ # at a time (if new one comes in we will park it)
92
+ def build(branch=nil)
93
+ if building?
94
+ # only if switched on to build all incoming requests
95
+ if !repo_config.buildallfile.to_s.empty?
96
+ # and there is no previous request
97
+ return if File.exist?(repo_config.buildallfile.to_s)
98
+ # we will mark awaiting builds
99
+ FileUtils.touch(repo_config.buildallfile.to_s)
100
+ end
101
+ # leave anyway because a current build runs
102
+ return
103
+ end
104
+ @current_build = Build.new(@project_path, @user, @project)
105
+ write_build 'current', @current_build
106
+ Thread.new { build!(branch) }
107
+ end
108
+
109
+ def open_pipe(cmd)
110
+ read, write = IO.pipe
111
+
112
+ pid = fork do
113
+ read.close
114
+ $stdout.reopen write
115
+ exec cmd
116
+ end
117
+
118
+ write.close
119
+
120
+ yield read, pid
121
+ end
122
+
123
+ # update git then run the build
124
+ def build!(branch=nil)
125
+ @git_branch = branch
126
+ build = @current_build
127
+ output = ''
128
+ git_update
129
+ build.sha = git_sha
130
+ build.branch = git_branch
131
+ write_build 'current', build
132
+
133
+ open_pipe("cd #{@project_path} && #{runner_command} 2>&1") do |pipe, pid|
134
+ puts "#{Time.now.to_i}: Building #{build.branch} at #{build.short_sha}: pid=#{pid}"
135
+
136
+ build.pid = pid
137
+ write_build 'current', build
138
+ output = pipe.read
139
+ end
140
+
141
+ Process.waitpid(build.pid, 1)
142
+ status = $?.exitstatus.to_i
143
+ @current_build = build
144
+ puts "#{Time.now.to_i}: Built #{build.short_sha}: status=#{status}"
145
+
146
+ status == 0 ? build_worked(output) : build_failed('', output)
147
+ rescue Object => e
148
+ puts "Exception building: #{e.message} (#{e.class})"
149
+ build_failed('', e.to_s)
150
+ end
151
+
152
+ # shellin' out
153
+ def runner_command
154
+ runner = repo_config.runner.to_s
155
+ runner == '' ? "rake -s test:units" : runner
156
+ end
157
+
158
+ def git_sha
159
+ `cd #{@project_path} && git rev-parse origin/#{git_branch}`.chomp
160
+ end
161
+
162
+ def git_update
163
+ `cd #{@project_path} && git fetch origin && git reset --hard origin/#{git_branch}`
164
+ run_hook "after-reset"
165
+ end
166
+
167
+ def git_user_and_project
168
+ Config.remote(@project_path).origin.url.to_s.chomp('.git').split(':')[-1].split('/')[-2, 2]
169
+ end
170
+
171
+ def git_branch
172
+ return @git_branch if @git_branch
173
+ branch = repo_config.branch.to_s
174
+ @git_branch = branch == '' ? "master" : branch
175
+ end
176
+
177
+ # massage our repo
178
+ def run_hook(hook)
179
+ if File.exists?(file=path_in_project(".git/hooks/#{hook}")) && File.executable?(file)
180
+ data =
181
+ if @last_build && @last_build.commit
182
+ {
183
+ "MESSAGE" => @last_build.commit.message,
184
+ "AUTHOR" => @last_build.commit.author,
185
+ "SHA" => @last_build.commit.sha,
186
+ "OUTPUT" => @last_build.env_output
187
+ }
188
+ else
189
+ {}
190
+ end
191
+
192
+ ENV.clear
193
+ data.each{ |k, v| ENV[k] = v }
194
+ `cd #{@project_path} && sh #{file}`
195
+ end
196
+ end
197
+
198
+ # restore current / last build state from disk.
199
+ def restore
200
+ @last_build = read_build('last')
201
+ @current_build = read_build('current')
202
+
203
+ Process.kill(0, @current_build.pid) if @current_build && @current_build.pid
204
+ rescue Errno::ESRCH
205
+ # build pid isn't running anymore. assume previous
206
+ # server died and reset.
207
+ @current_build = nil
208
+ end
209
+
210
+ def path_in_project(path)
211
+ File.join(@project_path, path)
212
+ end
213
+
214
+ # write build info for build to file.
215
+ def write_build(name, build)
216
+ filename = path_in_project(".git/builds/#{name}")
217
+ Dir.mkdir path_in_project('.git/builds') unless File.directory?(path_in_project('.git/builds'))
218
+ if build
219
+ build.dump filename
220
+ elsif File.exist?(filename)
221
+ File.unlink filename
222
+ end
223
+ end
224
+
225
+ def repo_config
226
+ Config.cijoe(@project_path)
227
+ end
228
+
229
+ # load build info from file.
230
+ def read_build(name)
231
+ Build.load(path_in_project(".git/builds/#{name}"), @project_path)
232
+ end
233
+ end