bpci 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ Copyright (c) 2012-present breakpoint-eval.org
2
+ Copyright (c) 2009 Chris Wanstrath (cijoe, from which bpci was forked)
3
+
4
+ Permission is hereby granted, free of charge, to any person obtaining
5
+ a copy of this software and associated documentation files (the
6
+ "Software"), to deal in the Software without restriction, including
7
+ without limitation the rights to use, copy, modify, merge, publish,
8
+ distribute, sublicense, and/or sell copies of the Software, and to
9
+ permit persons to whom the Software is furnished to do so, subject to
10
+ the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be
13
+ included in all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,136 @@
1
+ Breakpoint CI (bpci)
2
+ ====================
3
+
4
+ Breakpoint CI is a [Continuous
5
+ Integration](http://en.wikipedia.org/wiki/Continuous_integration)
6
+ server based on [defunkt/cijoe](https://github.com/defunkt/cijoe) that
7
+ we (Breakpoint developers) have:
8
+
9
+ 1. Made work.
10
+ 2. Tweaked/added things to our needs.
11
+ 3. Removed things that we don't (e.g. campfire support).
12
+
13
+ Quickstart
14
+ ----------
15
+
16
+ RubyGems:
17
+
18
+ $ gem install bpci
19
+ $ git clone git://github.com/you/yourrepo.git
20
+ $ bpci yourrepo
21
+
22
+ Boom. Navigate to <http://localhost:4567> to see bpci in action.
23
+ Check `bpci -h` for other options.
24
+
25
+ Basically you need to run `bpci` and hand it the path to a git
26
+ repo. Make sure this isn't a shared repo: bpci needs to own it.
27
+
28
+ bpci looks for various git config settings in the repo you hand it. For
29
+ instance, you can tell bpci what command to run by setting
30
+ `bpci.runner`:
31
+
32
+ $ git config --add bpci.runner "rake -s test:units"
33
+
34
+ bpci doesn't care about Ruby, Python, or whatever. As long as the
35
+ runner returns a non-zero exit status on fail and a zero on success,
36
+ everyone is happy.
37
+
38
+ Need to do some massaging of your repo before the tests run, like
39
+ maybe swapping in a new database.yml? No problem - bpci will try to
40
+ run `.git/hooks/after-reset` if it exists before each build phase.
41
+ Do it in there. Just make sure it's executable.
42
+
43
+ Want to notify IRC or email on test pass or failure? bpci will run
44
+ `.git/hooks/build-failed` or `.git/hooks/build-worked` if they exist
45
+ and are executable on build pass / fail. They're just shell scripts -
46
+ put whatever you want in there.
47
+
48
+ Tip: your repo's `HEAD` will point to the commit used to run the
49
+ build. Pull any metadata you want out of that scro.
50
+
51
+ ** WARNING ** Do not run this against a git repo that has unpushed
52
+ commits, as this will do a hard reset against the github remote and
53
+ wipe out unpushed changes.
54
+
55
+ Other Branches
56
+ ----------------------
57
+
58
+ Want bpci to run against a branch other than `master`? No problem:
59
+
60
+ $ git config --add bpci.branch deploy
61
+
62
+
63
+ Queueing
64
+ ----------------------------------------
65
+
66
+ bpci runs just one build at the time. If you expect concurrent push's
67
+ to your repo and want joe to build each in a kind of queue, just set:
68
+
69
+ $ git config --add bpci.buildqueue true
70
+
71
+ bpci will save requests while another build runs. If more than one push
72
+ hits bpci, it just picks the last after finishing the prior.
73
+
74
+
75
+ Campfire
76
+ -------------
77
+
78
+ ** We are removing Campfire support from bpci **
79
+
80
+
81
+ Checkin' Status
82
+ ----------------------
83
+
84
+ Want to see how your build's doing without any of this fancy UI crap?
85
+ Ping bpci for the lowdown:
86
+
87
+ curl http://localhost:4567/ping
88
+
89
+ bpci will return `200 OK` if all is quiet on the Western Front. If
90
+ bpci's busy building or your last build failed, you'll get `412
91
+ PRECONDITION FAILED`.
92
+
93
+
94
+ Multiple Projects
95
+ ------------------------
96
+
97
+ Want CI for multiple projects? Just start multiple instances of bpci!
98
+ It can run on any port - try `bpci -h` for more options.
99
+
100
+ If you're using Passenger, see [this blog post from the upstream project](http://chrismdp.github.com/2010/03/multiple-ci-joes-with-rack-and-passenger/).
101
+
102
+
103
+ HTTP Auth
104
+ ----------------
105
+
106
+ Worried about people triggering your builds? Setup HTTP auth:
107
+
108
+ $ git config --add bpci.user your_username
109
+ $ git config --add bpci.pass your_password
110
+
111
+
112
+ GitHub Integration
113
+ --------------------------
114
+
115
+ Any POST to bpci will trigger a build. If you are hiding bpci behind
116
+ HTTP auth, that's okay - GitHub knows how to authenticate properly.
117
+
118
+ ![Post-Receive URL](http://img.skitch.com/20090806-d2bxrk733gqu8m11tf4kyir5d8.png)
119
+
120
+ You can find the Post-Receive option under the 'Service Hooks' subtab
121
+ of your project's "Admin" tab.
122
+
123
+
124
+ Daemonize
125
+ ----------------
126
+
127
+ Want to run bpci as a daemon? Use `nohup`:
128
+
129
+ $ nohup bpci -p 4444 repo &
130
+
131
+
132
+ Questions? Concerns?
133
+ ---------------------------------
134
+
135
+ [Issues](http://github.com/breakpoint-eval/bpci/issues)
136
+ [Upstream](http://github.com/defunkt/cijoe)
@@ -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 'rdoc/task'
24
+ RDoc::Task.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 'bpci'
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#{BPCI::Version}"
43
+ exit
44
+ end
45
+ end
46
+ end
47
+
48
+ options = Choice.choices
49
+
50
+ BPCI::Server.start(options[:host], options[:port], File.expand_path(Choice.rest[0].to_s))
@@ -0,0 +1,227 @@
1
+ ##
2
+ # Breakpoint CI
3
+ #
4
+ # Fork of defunkt/cijoe, modified, hacked, and customized to our needs.
5
+ #
6
+ # With any luck, it might actually work.
7
+ #
8
+
9
+ require 'bpci/version'
10
+ require 'bpci/config'
11
+ require 'bpci/commit'
12
+ require 'bpci/build'
13
+ require 'bpci/server'
14
+ require 'bpci/queue'
15
+ require 'bpci/irc'
16
+
17
+ class BPCI
18
+ attr_reader :user, :project, :url, :current_build, :last_build
19
+
20
+ def initialize(project_path)
21
+ @project_path = File.expand_path(project_path)
22
+
23
+ @user, @project = git_user_and_project
24
+ @url = "http://github.com/#{@user}/#{@project}"
25
+
26
+ @irc = BPCI::IRCBot.new(@project_path)
27
+
28
+ @last_build = nil
29
+ @current_build = nil
30
+ @queue = Queue.new(!repo_config.buildqueue.to_s.empty?, true)
31
+
32
+ trap("INT") { stop }
33
+ end
34
+
35
+ # is a build running?
36
+ def building?
37
+ !!@current_build
38
+ end
39
+
40
+ # the pid of the running child process
41
+ def pid
42
+ building? and current_build.pid
43
+ end
44
+
45
+ # kill the child and exit
46
+ def stop
47
+ Process.kill(9, pid) if pid
48
+ exit!
49
+ end
50
+
51
+ # build callbacks
52
+ def build_failed(output, error)
53
+ finish_build :failed, "#{error}\n\n#{output}"
54
+ @irc.broadcast "Build of #{2.chr}#{@project}#{2.chr} finished: #{3.chr}4FAILED#{3.chr} - took #{@last_build.duration} seconds."
55
+ run_hook "build-failed"
56
+ end
57
+
58
+ def build_successful(output)
59
+ finish_build :successful, output
60
+ @irc.broadcast "Build of #{2.chr}#{@project}#{2.chr} finished: #{3.chr}9SUCCEEDED#{3.chr} - took #{@last_build.duration} seconds."
61
+ run_hook "build-successful"
62
+ end
63
+
64
+ def finish_build(status, output)
65
+ @current_build.finished_at = Time.now
66
+ @current_build.status = status
67
+ @current_build.output = output
68
+ @last_build = @current_build
69
+
70
+ @current_build = nil
71
+
72
+ # Obtain the latest file ID by globbing and using some ugly hacks.
73
+ old_builds = Dir.entries(path_in_project(".git/builds/"))
74
+ newest_file_id = old_builds.keep_if {|z| z =~ /^\d/}.collect {|z| z.split('-')[0].to_i}.max || 0
75
+ write_build "#{newest_file_id + 1}-#{@last_build.sha[0,7]}", @last_build
76
+
77
+ write_build 'current', @current_build
78
+ write_build 'last', @last_build
79
+
80
+ build(@queue.next_branch_to_build) if @queue.waiting?
81
+ end
82
+
83
+ # run the build but make sure only one is running
84
+ # at a time (if new one comes in we will park it)
85
+ def build(branch=nil)
86
+ if building?
87
+ @queue.append_unless_already_exists(branch)
88
+ @irc.broadcast "#{3.chr}8Queueing new build#{3.chr} of #{2.chr}#{@project}#{2.chr}" if @queue.enabled?
89
+ # leave anyway because a current build runs
90
+ return
91
+ end
92
+ @current_build = Build.new(@project_path, @user, @project)
93
+ @irc.broadcast "#{3.chr}12Starting build#{3.chr} of #{2.chr}#{@project}#{2.chr} at: #{@current_build.started_at}"
94
+ write_build 'current', @current_build
95
+ Thread.new { build!(branch) }
96
+ end
97
+
98
+ def open_pipe(cmd)
99
+ read, write = IO.pipe
100
+
101
+ pid = fork do
102
+ read.close
103
+ $stdout.reopen write
104
+ exec cmd
105
+ end
106
+
107
+ write.close
108
+
109
+ yield read, pid
110
+ end
111
+
112
+ # update git then run the build
113
+ def build!(branch=nil)
114
+ @git_branch = branch
115
+ build = @current_build
116
+ output = ''
117
+ git_update
118
+ build.sha = git_sha
119
+ build.branch = git_branch
120
+ write_build 'current', build
121
+
122
+ open_pipe("cd #{@project_path} && #{runner_command} 2>&1") do |pipe, pid|
123
+ puts "#{Time.now.to_i}: Building #{build.branch} at #{build.short_sha}: pid=#{pid}"
124
+
125
+ build.pid = pid
126
+ write_build 'current', build
127
+ output = pipe.read
128
+ end
129
+
130
+ Process.waitpid(build.pid, 1)
131
+ status = $?.exitstatus.to_i
132
+ @current_build = build
133
+ puts "#{Time.now.to_i}: Built #{build.short_sha}: status=#{status}"
134
+
135
+ status == 0 ? build_successful(output) : build_failed('', output)
136
+ rescue Object => e
137
+ puts "Exception building: #{e.message} (#{e.class})"
138
+ build_failed('', e.to_s)
139
+ end
140
+
141
+ # shellin' out
142
+ def runner_command
143
+ runner = repo_config.runner.to_s
144
+ runner == '' ? "rake -s test:units" : runner
145
+ end
146
+
147
+ def git_sha
148
+ `cd #{@project_path} && git rev-parse origin/#{git_branch}`.chomp
149
+ end
150
+
151
+ def git_update
152
+ `cd #{@project_path} && git fetch origin && git reset --hard origin/#{git_branch}`
153
+ run_hook "after-reset"
154
+ end
155
+
156
+ def git_user_and_project
157
+ Config.remote(@project_path).origin.url.to_s.chomp('.git').split(':')[-1].split('/')[-2, 2]
158
+ end
159
+
160
+ def git_branch
161
+ return @git_branch if @git_branch
162
+ branch = repo_config.branch.to_s
163
+ @git_branch = branch == '' ? "master" : branch
164
+ end
165
+
166
+ # massage our repo
167
+ def run_hook(hook)
168
+ if File.exists?(file=path_in_project(".git/hooks/#{hook}")) && File.executable?(file)
169
+ data =
170
+ if @last_build && @last_build.commit
171
+ {
172
+ "MESSAGE" => @last_build.commit.message,
173
+ "AUTHOR" => @last_build.commit.author,
174
+ "SHA" => @last_build.commit.sha,
175
+ "OUTPUT" => @last_build.env_output
176
+ }
177
+ else
178
+ {}
179
+ end
180
+
181
+ orig_ENV = ENV.to_hash
182
+ ENV.clear
183
+ data.each{ |k, v| ENV[k] = v }
184
+ output = `cd #{@project_path} && sh #{file}`
185
+
186
+ ENV.clear
187
+ orig_ENV.to_hash.each{ |k, v| ENV[k] = v}
188
+ output
189
+ end
190
+ end
191
+
192
+ # restore current / last build state from disk.
193
+ def restore
194
+ @last_build = read_build('last')
195
+ @current_build = read_build('current')
196
+
197
+ Process.kill(0, @current_build.pid) if @current_build && @current_build.pid
198
+ rescue Errno::ESRCH
199
+ # build pid isn't running anymore. assume previous
200
+ # server died and reset.
201
+ @current_build = nil
202
+ end
203
+
204
+ def path_in_project(path)
205
+ File.join(@project_path, path)
206
+ end
207
+
208
+ # write build info for build to file.
209
+ def write_build(name, build)
210
+ filename = path_in_project(".git/builds/#{name}")
211
+ Dir.mkdir path_in_project('.git/builds') unless File.directory?(path_in_project('.git/builds'))
212
+ if build
213
+ build.dump filename
214
+ elsif File.exist?(filename)
215
+ File.unlink filename
216
+ end
217
+ end
218
+
219
+ def repo_config
220
+ Config.bpci(@project_path)
221
+ end
222
+
223
+ # load build info from file.
224
+ def read_build(name)
225
+ Build.load(path_in_project(".git/builds/#{name}"), @project_path)
226
+ end
227
+ end