glennr-cijoe 0.4.3
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/.gitignore +6 -0
- data/LICENSE +20 -0
- data/README.markdown +170 -0
- data/Rakefile +68 -0
- data/bin/cijoe +51 -0
- data/cijoe.gemspec +77 -0
- data/deps.rip +4 -0
- data/examples/build-failed +22 -0
- data/examples/build-worked +22 -0
- data/examples/cijoe.ru +15 -0
- data/examples/cijoed +53 -0
- data/glennr-cijoe.gemspec +82 -0
- data/lib/cijoe.rb +233 -0
- data/lib/cijoe/build.rb +62 -0
- data/lib/cijoe/campfire.rb +70 -0
- data/lib/cijoe/commit.rb +27 -0
- data/lib/cijoe/config.rb +43 -0
- data/lib/cijoe/public/favicon.ico +0 -0
- data/lib/cijoe/public/octocat.png +0 -0
- data/lib/cijoe/public/screen.css +213 -0
- data/lib/cijoe/server.rb +91 -0
- data/lib/cijoe/version.rb +3 -0
- data/lib/cijoe/views/template.erb +70 -0
- data/test/helper.rb +12 -0
- data/test/test_cijoe.rb +17 -0
- data/test/test_cijoe_server.rb +50 -0
- metadata +150 -0
data/examples/cijoe.ru
ADDED
@@ -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
|
data/examples/cijoed
ADDED
@@ -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,82 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = %q{glennr-cijoe}
|
8
|
+
s.version = "0.4.3"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["Chris Wanstrath"]
|
12
|
+
s.date = %q{2010-07-22}
|
13
|
+
s.default_executable = %q{cijoe}
|
14
|
+
s.description = %q{CI Joe is a simple Continuous Integration server.}
|
15
|
+
s.email = %q{chris@ozmm.org}
|
16
|
+
s.executables = ["cijoe"]
|
17
|
+
s.extra_rdoc_files = [
|
18
|
+
"LICENSE",
|
19
|
+
"README.markdown"
|
20
|
+
]
|
21
|
+
s.files = [
|
22
|
+
".gitignore",
|
23
|
+
"LICENSE",
|
24
|
+
"README.markdown",
|
25
|
+
"Rakefile",
|
26
|
+
"bin/cijoe",
|
27
|
+
"cijoe.gemspec",
|
28
|
+
"deps.rip",
|
29
|
+
"examples/build-failed",
|
30
|
+
"examples/build-worked",
|
31
|
+
"examples/cijoe.ru",
|
32
|
+
"examples/cijoed",
|
33
|
+
"glennr-cijoe.gemspec",
|
34
|
+
"lib/cijoe.rb",
|
35
|
+
"lib/cijoe/build.rb",
|
36
|
+
"lib/cijoe/campfire.rb",
|
37
|
+
"lib/cijoe/commit.rb",
|
38
|
+
"lib/cijoe/config.rb",
|
39
|
+
"lib/cijoe/public/favicon.ico",
|
40
|
+
"lib/cijoe/public/octocat.png",
|
41
|
+
"lib/cijoe/public/screen.css",
|
42
|
+
"lib/cijoe/server.rb",
|
43
|
+
"lib/cijoe/version.rb",
|
44
|
+
"lib/cijoe/views/template.erb",
|
45
|
+
"test/helper.rb",
|
46
|
+
"test/test_cijoe.rb",
|
47
|
+
"test/test_cijoe_server.rb"
|
48
|
+
]
|
49
|
+
s.homepage = %q{http://github.com/defunkt/cijoe}
|
50
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
51
|
+
s.require_paths = ["lib"]
|
52
|
+
s.rubygems_version = %q{1.3.7}
|
53
|
+
s.summary = %q{CI Joe is a simple Continuous Integration server.}
|
54
|
+
s.test_files = [
|
55
|
+
"test/helper.rb",
|
56
|
+
"test/test_cijoe.rb",
|
57
|
+
"test/test_cijoe_server.rb"
|
58
|
+
]
|
59
|
+
|
60
|
+
if s.respond_to? :specification_version then
|
61
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
62
|
+
s.specification_version = 3
|
63
|
+
|
64
|
+
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
65
|
+
s.add_runtime_dependency(%q<choice>, [">= 0"])
|
66
|
+
s.add_runtime_dependency(%q<sinatra>, [">= 0"])
|
67
|
+
s.add_runtime_dependency(%q<tinder>, [">= 0"])
|
68
|
+
s.add_development_dependency(%q<rack-test>, [">= 0"])
|
69
|
+
else
|
70
|
+
s.add_dependency(%q<choice>, [">= 0"])
|
71
|
+
s.add_dependency(%q<sinatra>, [">= 0"])
|
72
|
+
s.add_dependency(%q<tinder>, [">= 0"])
|
73
|
+
s.add_dependency(%q<rack-test>, [">= 0"])
|
74
|
+
end
|
75
|
+
else
|
76
|
+
s.add_dependency(%q<choice>, [">= 0"])
|
77
|
+
s.add_dependency(%q<sinatra>, [">= 0"])
|
78
|
+
s.add_dependency(%q<tinder>, [">= 0"])
|
79
|
+
s.add_dependency(%q<rack-test>, [">= 0"])
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
data/lib/cijoe.rb
ADDED
@@ -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 && 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 && 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
|
93
|
+
if building?
|
94
|
+
# only if switched on to build all incoming requests
|
95
|
+
if repo_config.buildallfile
|
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! }
|
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!
|
125
|
+
build = @current_build
|
126
|
+
output = ''
|
127
|
+
git_update
|
128
|
+
build.sha = git_sha
|
129
|
+
write_build 'current', build
|
130
|
+
|
131
|
+
open_pipe("cd #{@project_path} && #{runner_command} 2>&1") do |pipe, pid|
|
132
|
+
puts "#{Time.now.to_i}: Building #{build.short_sha}: pid=#{pid}"
|
133
|
+
|
134
|
+
build.pid = pid
|
135
|
+
write_build 'current', build
|
136
|
+
output = pipe.read
|
137
|
+
end
|
138
|
+
|
139
|
+
Process.waitpid(build.pid)
|
140
|
+
status = $?.exitstatus.to_i
|
141
|
+
puts "#{Time.now.to_i}: Built #{build.short_sha}: status=#{status}"
|
142
|
+
|
143
|
+
status == 0 ? build_worked(output) : build_failed('', output)
|
144
|
+
rescue Object => e
|
145
|
+
puts "Exception building: #{e.message} (#{e.class})"
|
146
|
+
build_failed('', e.to_s)
|
147
|
+
end
|
148
|
+
|
149
|
+
# shellin' out
|
150
|
+
def runner_command
|
151
|
+
runner = repo_config.runner.to_s
|
152
|
+
runner == '' ? "rake -s test:units" : runner
|
153
|
+
end
|
154
|
+
|
155
|
+
def git_sha
|
156
|
+
`cd #{@project_path} && git rev-parse origin/#{git_branch}`.chomp
|
157
|
+
end
|
158
|
+
|
159
|
+
def git_update
|
160
|
+
`cd #{@project_path} && git fetch origin && git reset --hard origin/#{git_branch}`
|
161
|
+
run_hook "after-reset"
|
162
|
+
end
|
163
|
+
|
164
|
+
def git_user_and_project
|
165
|
+
Config.remote(@project_path).origin.url.to_s.chomp('.git').split(':')[-1].split('/')[-2, 2]
|
166
|
+
end
|
167
|
+
|
168
|
+
def git_branch
|
169
|
+
branch = repo_config.branch.to_s
|
170
|
+
branch == '' ? "master" : branch
|
171
|
+
end
|
172
|
+
|
173
|
+
# massage our repo
|
174
|
+
def run_hook(hook)
|
175
|
+
if File.exists?(file=path_in_project(".git/hooks/#{hook}")) && File.executable?(file)
|
176
|
+
data =
|
177
|
+
if @last_build && @last_build.commit
|
178
|
+
{
|
179
|
+
"MESSAGE" => @last_build.commit.message,
|
180
|
+
"AUTHOR" => @last_build.commit.author,
|
181
|
+
"SHA" => @last_build.commit.sha,
|
182
|
+
"OUTPUT" => @last_build.clean_output
|
183
|
+
}
|
184
|
+
else
|
185
|
+
{}
|
186
|
+
end
|
187
|
+
|
188
|
+
data.each{ |k, v| ENV[k] = v }
|
189
|
+
`cd #{@project_path} && sh #{file}`
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
# restore current / last build state from disk.
|
194
|
+
def restore
|
195
|
+
unless @last_build
|
196
|
+
@last_build = read_build('last')
|
197
|
+
end
|
198
|
+
|
199
|
+
unless @current_build
|
200
|
+
@current_build = read_build('current')
|
201
|
+
end
|
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
|
data/lib/cijoe/build.rb
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
class CIJoe
|
4
|
+
class Build < Struct.new(:project_path, :user, :project, :started_at, :finished_at, :sha, :status, :output, :pid)
|
5
|
+
def initialize(*args)
|
6
|
+
super
|
7
|
+
self.started_at ||= Time.now
|
8
|
+
end
|
9
|
+
|
10
|
+
def status
|
11
|
+
return super if started_at && finished_at
|
12
|
+
:building
|
13
|
+
end
|
14
|
+
|
15
|
+
def failed?
|
16
|
+
status == :failed
|
17
|
+
end
|
18
|
+
|
19
|
+
def worked?
|
20
|
+
status == :worked
|
21
|
+
end
|
22
|
+
|
23
|
+
def building?
|
24
|
+
status == :building
|
25
|
+
end
|
26
|
+
|
27
|
+
def duration
|
28
|
+
return if building?
|
29
|
+
finished_at - started_at
|
30
|
+
end
|
31
|
+
|
32
|
+
def short_sha
|
33
|
+
if sha
|
34
|
+
sha[0,7]
|
35
|
+
else
|
36
|
+
"<unknown>"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def clean_output
|
41
|
+
output.gsub(/\e\[.+?m/, '').strip
|
42
|
+
end
|
43
|
+
|
44
|
+
def commit
|
45
|
+
return if sha.nil?
|
46
|
+
@commit ||= Commit.new(sha, user, project, project_path)
|
47
|
+
end
|
48
|
+
|
49
|
+
def dump(file)
|
50
|
+
config = [user, project, started_at, finished_at, sha, status, output, pid]
|
51
|
+
data = YAML.dump(config)
|
52
|
+
File.open(file, 'wb') { |io| io.write(data) }
|
53
|
+
end
|
54
|
+
|
55
|
+
def self.load(file, project_path)
|
56
|
+
if File.exist?(file)
|
57
|
+
config = YAML.load(File.read(file)).unshift(project_path)
|
58
|
+
new *config
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|