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 +21 -0
- data/README.md +136 -0
- data/Rakefile +31 -0
- data/bin/bpci +50 -0
- data/lib/bpci.rb +227 -0
- data/lib/bpci/#config.rb# +43 -0
- data/lib/bpci/build.rb +67 -0
- data/lib/bpci/commit.rb +27 -0
- data/lib/bpci/config.rb +43 -0
- data/lib/bpci/irc.rb +58 -0
- data/lib/bpci/irc.rb~ +42 -0
- data/lib/bpci/public/bootstrap/css/bootstrap-responsive.min.css +12 -0
- data/lib/bpci/public/bootstrap/css/bootstrap.min.css +689 -0
- data/lib/bpci/public/bootstrap/img/glyphicons-halflings-white.png +0 -0
- data/lib/bpci/public/bootstrap/img/glyphicons-halflings.png +0 -0
- data/lib/bpci/public/bootstrap/js/bootstrap.min.js +6 -0
- data/lib/bpci/public/favicon.ico +0 -0
- data/lib/bpci/queue.rb +55 -0
- data/lib/bpci/server.rb +115 -0
- data/lib/bpci/version.rb +3 -0
- data/lib/bpci/views/json.erb +14 -0
- data/lib/bpci/views/template.erb +135 -0
- data/test/fixtures/payload.json +52 -0
- data/test/helper.rb +47 -0
- data/test/test_cijoe.rb +17 -0
- data/test/test_cijoe_queue.rb +28 -0
- data/test/test_cijoe_server.rb +128 -0
- data/test/test_hooks.rb +81 -0
- metadata +143 -0
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.
|
data/README.md
ADDED
@@ -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
|
+

|
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)
|
data/Rakefile
ADDED
@@ -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
|
data/bin/bpci
ADDED
@@ -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))
|
data/lib/bpci.rb
ADDED
@@ -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
|