bpci 0.0.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,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