nyc-ruby-meetup_cijoe 0.5.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 +20 -0
- data/README.md +171 -0
- data/bin/cicobra +51 -0
- data/lib/cijoe/build.rb +67 -0
- data/lib/cijoe/campfire.rb +77 -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 +74 -0
- data/lib/cijoe.rb +232 -0
- data/test/helper.rb +14 -0
- data/test/test_cijoe.rb +17 -0
- data/test/test_cijoe_server.rb +73 -0
- metadata +136 -0
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.
|
data/README.md
ADDED
@@ -0,0 +1,171 @@
|
|
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
|
+
[](http://nerduo.com/thebattle/)
|
11
|
+
|
12
|
+
([Buy the shirt](http://nerduo.com/thebattle/))
|
13
|
+
|
14
|
+
Quickstart
|
15
|
+
----------
|
16
|
+
|
17
|
+
RubyGems:
|
18
|
+
|
19
|
+
$ gem install nyc-ruby-meetup_cijoe
|
20
|
+
$ git clone git://github.com/you/yourrepo.git
|
21
|
+
$ cicobra yourrepo
|
22
|
+
|
23
|
+
Boom. Navigate to <http://localhost:4567> to see Joe in action.
|
24
|
+
Check `cicobra -h` for other options.
|
25
|
+
|
26
|
+
Basically you need to run `cicobra` 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
|
+

|
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 cicobra -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
|
+
* [Cerberus](http://cerberus.rubyforge.org/)
|
151
|
+
* [Integrity](http://integrityapp.com/)
|
152
|
+
* [CruiseControl.rb](http://cruisecontrolrb.thoughtworks.com/)
|
153
|
+
* [BuildBot](http://buildbot.net/trac)
|
154
|
+
* [Signal](http://www.github.com/dcrec1/signal)
|
155
|
+
|
156
|
+
|
157
|
+
Screenshots
|
158
|
+
------------------
|
159
|
+
|
160
|
+

|
161
|
+
|
162
|
+

|
163
|
+
|
164
|
+
|
165
|
+
Questions? Concerns?
|
166
|
+
---------------------------------
|
167
|
+
|
168
|
+
[Issues](http://github.com/defunkt/cijoe/issues) or [the mailing list](http://groups.google.com/group/cijoe).
|
169
|
+
|
170
|
+
|
171
|
+
( Chris Wanstrath :: chris@ozmm.org )
|
data/bin/cicobra
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
$LOAD_PATH.unshift File.dirname(__FILE__) + '/../lib'
|
3
|
+
|
4
|
+
require 'choice'
|
5
|
+
|
6
|
+
Choice.options do
|
7
|
+
banner "Usage: #{File.basename(__FILE__)} [-hpv] path_to_git_repo"
|
8
|
+
header ''
|
9
|
+
header 'Server options:'
|
10
|
+
|
11
|
+
option :host do
|
12
|
+
d = "0.0.0.0"
|
13
|
+
short '-h'
|
14
|
+
long '--host=HOST'
|
15
|
+
desc "The hostname or ip of the host to bind to (default #{d})"
|
16
|
+
default d
|
17
|
+
end
|
18
|
+
|
19
|
+
option :port do
|
20
|
+
d = 4567
|
21
|
+
short '-p'
|
22
|
+
long '--port=PORT'
|
23
|
+
desc "The port to listen on (default #{d})"
|
24
|
+
cast Integer
|
25
|
+
default d
|
26
|
+
end
|
27
|
+
|
28
|
+
separator ''
|
29
|
+
separator 'Common options: '
|
30
|
+
|
31
|
+
option :help do
|
32
|
+
long '--help'
|
33
|
+
desc 'Show this message'
|
34
|
+
end
|
35
|
+
|
36
|
+
option :version do
|
37
|
+
short '-v'
|
38
|
+
long '--version'
|
39
|
+
desc 'Show version'
|
40
|
+
action do
|
41
|
+
puts "#{File.basename(__FILE__)} v#{CIJoe::Version}"
|
42
|
+
exit
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
options = Choice.choices
|
48
|
+
|
49
|
+
require 'cijoe'
|
50
|
+
|
51
|
+
CIJoe::Server.start(options[:host], options[:port], File.expand_path(Choice.rest[0]))
|
data/lib/cijoe/build.rb
ADDED
@@ -0,0 +1,67 @@
|
|
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 env_output
|
45
|
+
out = clean_output
|
46
|
+
out.size > 100_000 ? out[-100_000,100_000] : out
|
47
|
+
end
|
48
|
+
|
49
|
+
def commit
|
50
|
+
return if sha.nil?
|
51
|
+
@commit ||= Commit.new(sha, user, project, project_path)
|
52
|
+
end
|
53
|
+
|
54
|
+
def dump(file)
|
55
|
+
config = [user, project, started_at, finished_at, sha, status, output, pid]
|
56
|
+
data = YAML.dump(config)
|
57
|
+
File.open(file, 'wb') { |io| io.write(data) }
|
58
|
+
end
|
59
|
+
|
60
|
+
def self.load(file, project_path)
|
61
|
+
if File.exist?(file)
|
62
|
+
config = YAML.load(File.read(file)).unshift(project_path)
|
63
|
+
new *config
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
class CIJoe
|
2
|
+
module Campfire
|
3
|
+
def self.activate(project_path)
|
4
|
+
@project_path = project_path
|
5
|
+
|
6
|
+
if valid_config?
|
7
|
+
require 'tinder'
|
8
|
+
|
9
|
+
CIJoe::Build.class_eval do
|
10
|
+
include CIJoe::Campfire
|
11
|
+
end
|
12
|
+
|
13
|
+
puts "Loaded Campfire notifier"
|
14
|
+
elsif ENV['RACK_ENV'] != 'test'
|
15
|
+
puts "Can't load Campfire notifier."
|
16
|
+
puts "Please add the following to your project's .git/config:"
|
17
|
+
puts "[campfire]"
|
18
|
+
puts "\tuser = your@campfire.email"
|
19
|
+
puts "\tpass = passw0rd"
|
20
|
+
puts "\tsubdomain = whatever"
|
21
|
+
puts "\troom = Awesomeness"
|
22
|
+
puts "\tssl = false"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.config
|
27
|
+
campfire_config = Config.new('campfire', @project_path)
|
28
|
+
@config ||= {
|
29
|
+
:subdomain => campfire_config.subdomain.to_s,
|
30
|
+
:user => campfire_config.user.to_s,
|
31
|
+
:pass => campfire_config.pass.to_s,
|
32
|
+
:room => campfire_config.room.to_s,
|
33
|
+
:ssl => campfire_config.ssl.to_s.strip == 'true'
|
34
|
+
}
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.valid_config?
|
38
|
+
%w( subdomain user pass room ).all? do |key|
|
39
|
+
!config[key.intern].empty?
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def notify
|
44
|
+
room.speak "#{short_message}. #{commit.url}"
|
45
|
+
room.paste full_message if failed?
|
46
|
+
room.leave
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
def room
|
51
|
+
@room ||= begin
|
52
|
+
config = Campfire.config
|
53
|
+
campfire = Tinder::Campfire.new(config[:subdomain],
|
54
|
+
:username => config[:user],
|
55
|
+
:password => config[:pass],
|
56
|
+
:ssl => config[:ssl] || false)
|
57
|
+
campfire.find_room_by_name(config[:room])
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def short_message
|
62
|
+
"Build #{short_sha} of #{project} " +
|
63
|
+
(worked? ? "was successful" : "failed") +
|
64
|
+
" (#{duration.to_i}s)"
|
65
|
+
end
|
66
|
+
|
67
|
+
def full_message
|
68
|
+
<<-EOM
|
69
|
+
Commit Message: #{commit.message}
|
70
|
+
Commit Date: #{commit.committed_at}
|
71
|
+
Commit Author: #{commit.author}
|
72
|
+
|
73
|
+
#{clean_output}
|
74
|
+
EOM
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
data/lib/cijoe/commit.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
class CIJoe
|
2
|
+
class Commit < Struct.new(:sha, :user, :project, :project_path)
|
3
|
+
def url
|
4
|
+
"http://github.com/#{user}/#{project}/commit/#{sha}"
|
5
|
+
end
|
6
|
+
|
7
|
+
def author
|
8
|
+
raw_commit_lines.grep(/Author:/).first.split(':', 2)[-1]
|
9
|
+
end
|
10
|
+
|
11
|
+
def committed_at
|
12
|
+
raw_commit_lines.grep(/Date:/).first.split(':', 2)[-1]
|
13
|
+
end
|
14
|
+
|
15
|
+
def message
|
16
|
+
raw_commit.split("\n\n", 3)[1].to_s.strip
|
17
|
+
end
|
18
|
+
|
19
|
+
def raw_commit
|
20
|
+
@raw_commit ||= `cd #{project_path} && git show #{sha}`.chomp
|
21
|
+
end
|
22
|
+
|
23
|
+
def raw_commit_lines
|
24
|
+
@raw_commit_lines ||= raw_commit.split("\n")
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
data/lib/cijoe/config.rb
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
class CIJoe
|
2
|
+
class Config
|
3
|
+
def self.method_missing(command, *args)
|
4
|
+
new(command, *args)
|
5
|
+
end
|
6
|
+
|
7
|
+
def initialize(command, project_path = nil, parent = nil)
|
8
|
+
@command = command
|
9
|
+
@parent = parent
|
10
|
+
@project_path = project_path || File.join(File.dirname(__FILE__), '../../')
|
11
|
+
end
|
12
|
+
|
13
|
+
def method_missing(command, *args)
|
14
|
+
Config.new(command, @project_path, self)
|
15
|
+
end
|
16
|
+
|
17
|
+
def to_s
|
18
|
+
git_command = "cd #{@project_path} && git config #{config_string}"
|
19
|
+
result = `#{git_command} 2>&1`.chomp
|
20
|
+
process_status = $?
|
21
|
+
|
22
|
+
if successful_command?(process_status) || config_command_with_empty_value?(result,process_status)
|
23
|
+
return result
|
24
|
+
else
|
25
|
+
raise "Error calling git config, is a recent version of git installed? Command: #{git_command.inspect}, Error: #{result.inspect}, Status: #{process_status.inspect}"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def config_string
|
30
|
+
@parent ? "#{@parent.config_string}.#{@command}" : @command
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def successful_command?(process_status)
|
36
|
+
process_status.exitstatus.to_i == 0
|
37
|
+
end
|
38
|
+
|
39
|
+
def config_command_with_empty_value?(result, process_status)
|
40
|
+
process_status.exitstatus.to_i == 1 && result.empty?
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
Binary file
|
Binary file
|
@@ -0,0 +1,213 @@
|
|
1
|
+
/*****************************************************************************/
|
2
|
+
/*
|
3
|
+
/* Common
|
4
|
+
/*
|
5
|
+
/*****************************************************************************/
|
6
|
+
|
7
|
+
/* Global Reset */
|
8
|
+
|
9
|
+
* {
|
10
|
+
margin: 0;
|
11
|
+
padding: 0;
|
12
|
+
}
|
13
|
+
|
14
|
+
html, body {
|
15
|
+
height: 100%;
|
16
|
+
}
|
17
|
+
|
18
|
+
body {
|
19
|
+
background-color: white;
|
20
|
+
font: 13.34px helvetica, arial, clean, sans-serif;
|
21
|
+
*font-size: small;
|
22
|
+
text-align: center;
|
23
|
+
}
|
24
|
+
|
25
|
+
h1, h2, h3, h4, h5, h6 {
|
26
|
+
font-size: 100%;
|
27
|
+
}
|
28
|
+
|
29
|
+
h1 {
|
30
|
+
margin-bottom: 1em;
|
31
|
+
}
|
32
|
+
|
33
|
+
h1 a {
|
34
|
+
text-decoration: none;
|
35
|
+
color: #000;
|
36
|
+
}
|
37
|
+
|
38
|
+
.failed, .color31 {
|
39
|
+
color: red !important;
|
40
|
+
}
|
41
|
+
|
42
|
+
.worked, .color32 {
|
43
|
+
color: green !important;
|
44
|
+
}
|
45
|
+
|
46
|
+
.errored, .color33 {
|
47
|
+
color: yellow !important;
|
48
|
+
}
|
49
|
+
|
50
|
+
p {
|
51
|
+
margin: 1em 0;
|
52
|
+
}
|
53
|
+
|
54
|
+
a {
|
55
|
+
color: #00a;
|
56
|
+
}
|
57
|
+
|
58
|
+
a:hover {
|
59
|
+
color: black;
|
60
|
+
}
|
61
|
+
|
62
|
+
a:visited {
|
63
|
+
color: #a0a;
|
64
|
+
}
|
65
|
+
|
66
|
+
table {
|
67
|
+
font-size: inherit;
|
68
|
+
font: 100%;
|
69
|
+
}
|
70
|
+
|
71
|
+
/*****************************************************************************/
|
72
|
+
/*
|
73
|
+
/* Home
|
74
|
+
/*
|
75
|
+
/*****************************************************************************/
|
76
|
+
|
77
|
+
ul.posts {
|
78
|
+
list-style-type: none;
|
79
|
+
margin-bottom: 2em;
|
80
|
+
}
|
81
|
+
|
82
|
+
ul.posts li {
|
83
|
+
line-height: 1.75em;
|
84
|
+
}
|
85
|
+
|
86
|
+
ul.posts .date,
|
87
|
+
ul.posts .duration {
|
88
|
+
color: #aaa;
|
89
|
+
font-family: Monaco, "Courier New", monospace;
|
90
|
+
font-size: 80%;
|
91
|
+
}
|
92
|
+
|
93
|
+
/*****************************************************************************/
|
94
|
+
/*
|
95
|
+
/* Site
|
96
|
+
/*
|
97
|
+
/*****************************************************************************/
|
98
|
+
|
99
|
+
.site {
|
100
|
+
font-size: 110%;
|
101
|
+
text-align: justify;
|
102
|
+
width: 80%;
|
103
|
+
margin: 3em auto 2em auto;
|
104
|
+
line-height: 1.5em;
|
105
|
+
}
|
106
|
+
|
107
|
+
.title {
|
108
|
+
color: #a00;
|
109
|
+
font-weight: bold;
|
110
|
+
margin-bottom: 2em;
|
111
|
+
}
|
112
|
+
|
113
|
+
.site .title a {
|
114
|
+
color: #a00;
|
115
|
+
text-decoration: none;
|
116
|
+
}
|
117
|
+
|
118
|
+
.site .title a:hover {
|
119
|
+
color: black;
|
120
|
+
}
|
121
|
+
|
122
|
+
.site .title .extra {
|
123
|
+
color: #aaa;
|
124
|
+
text-decoration: none;
|
125
|
+
margin-left: 1em;
|
126
|
+
font-size: 0.9em;
|
127
|
+
}
|
128
|
+
|
129
|
+
.site .title a.extra:hover {
|
130
|
+
color: black;
|
131
|
+
}
|
132
|
+
|
133
|
+
.site .meta {
|
134
|
+
color: #aaa;
|
135
|
+
}
|
136
|
+
|
137
|
+
.site .footer {
|
138
|
+
font-size: 80%;
|
139
|
+
color: #666;
|
140
|
+
border-top: 4px solid #eee;
|
141
|
+
margin-top: 2em;
|
142
|
+
overflow: hidden;
|
143
|
+
}
|
144
|
+
|
145
|
+
.site .footer .contact {
|
146
|
+
float: left;
|
147
|
+
margin-right: 3em;
|
148
|
+
}
|
149
|
+
|
150
|
+
.site .footer .contact a {
|
151
|
+
color: #8085C1;
|
152
|
+
}
|
153
|
+
|
154
|
+
.site .footer .rss {
|
155
|
+
margin-top: 1.1em;
|
156
|
+
margin-right: -.2em;
|
157
|
+
float: right;
|
158
|
+
}
|
159
|
+
|
160
|
+
.site .footer .rss img {
|
161
|
+
border: 0;
|
162
|
+
}
|
163
|
+
|
164
|
+
/*****************************************************************************/
|
165
|
+
/*
|
166
|
+
/* Posts
|
167
|
+
/*
|
168
|
+
/*****************************************************************************/
|
169
|
+
|
170
|
+
#post {
|
171
|
+
|
172
|
+
}
|
173
|
+
|
174
|
+
/* standard */
|
175
|
+
|
176
|
+
#post pre {
|
177
|
+
border: 1px solid #ddd;
|
178
|
+
background-color: #eef;
|
179
|
+
padding: 0 .4em;
|
180
|
+
}
|
181
|
+
|
182
|
+
#post ul,
|
183
|
+
#post ol {
|
184
|
+
margin-left: 1.25em;
|
185
|
+
}
|
186
|
+
|
187
|
+
#post code {
|
188
|
+
border: 1px solid #ddd;
|
189
|
+
background-color: #eef;
|
190
|
+
font-size: 95%;
|
191
|
+
padding: 0 .2em;
|
192
|
+
}
|
193
|
+
|
194
|
+
#post pre code {
|
195
|
+
border: none;
|
196
|
+
}
|
197
|
+
|
198
|
+
/* terminal */
|
199
|
+
|
200
|
+
pre.terminal {
|
201
|
+
border: 1px solid black;
|
202
|
+
background-color: #333;
|
203
|
+
color: white;
|
204
|
+
padding: 5px;
|
205
|
+
overflow: auto;
|
206
|
+
word-wrap: break-word;
|
207
|
+
}
|
208
|
+
|
209
|
+
pre.terminal code {
|
210
|
+
font-family: 'Bitstream Vera Sans Mono', 'Courier', monospace;
|
211
|
+
background-color: #333;
|
212
|
+
}
|
213
|
+
|
data/lib/cijoe/server.rb
ADDED
@@ -0,0 +1,91 @@
|
|
1
|
+
require 'sinatra'
|
2
|
+
require 'erb'
|
3
|
+
|
4
|
+
class CIJoe
|
5
|
+
class Server < Sinatra::Base
|
6
|
+
attr_reader :joe
|
7
|
+
|
8
|
+
dir = File.dirname(File.expand_path(__FILE__))
|
9
|
+
|
10
|
+
set :views, "#{dir}/views"
|
11
|
+
set :public, "#{dir}/public"
|
12
|
+
set :static, true
|
13
|
+
set :lock, true
|
14
|
+
|
15
|
+
before { joe.restore }
|
16
|
+
|
17
|
+
get '/ping' do
|
18
|
+
if joe.building? || !joe.last_build || !joe.last_build.worked?
|
19
|
+
halt 412, (joe.building? || joe.last_build.nil?) ? "building" : joe.last_build.sha
|
20
|
+
end
|
21
|
+
|
22
|
+
joe.last_build.sha
|
23
|
+
end
|
24
|
+
|
25
|
+
get '/?' do
|
26
|
+
erb(:template, {}, :joe => joe)
|
27
|
+
end
|
28
|
+
|
29
|
+
post '/?' do
|
30
|
+
payload = params[:payload].to_s
|
31
|
+
if payload.empty? || payload.include?(joe.git_branch)
|
32
|
+
joe.build(params[:branch])
|
33
|
+
end
|
34
|
+
redirect request.path
|
35
|
+
end
|
36
|
+
|
37
|
+
|
38
|
+
helpers do
|
39
|
+
include Rack::Utils
|
40
|
+
alias_method :h, :escape_html
|
41
|
+
|
42
|
+
# thanks integrity!
|
43
|
+
def ansi_color_codes(string)
|
44
|
+
string.gsub("\e[0m", '</span>').
|
45
|
+
gsub(/\e\[(\d+)m/, "<span class=\"color\\1\">")
|
46
|
+
end
|
47
|
+
|
48
|
+
def pretty_time(time)
|
49
|
+
time.strftime("%Y-%m-%d %H:%M")
|
50
|
+
end
|
51
|
+
|
52
|
+
def cijoe_root
|
53
|
+
root = request.path
|
54
|
+
root = "" if root == "/"
|
55
|
+
root
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def initialize(*args)
|
60
|
+
super
|
61
|
+
check_project
|
62
|
+
@joe = CIJoe.new(options.project_path)
|
63
|
+
|
64
|
+
CIJoe::Campfire.activate(options.project_path)
|
65
|
+
end
|
66
|
+
|
67
|
+
def self.start(host, port, project_path)
|
68
|
+
set :project_path, project_path
|
69
|
+
CIJoe::Server.run! :host => host, :port => port
|
70
|
+
end
|
71
|
+
|
72
|
+
def self.project_path=(project_path)
|
73
|
+
user, pass = Config.cijoe(project_path).user.to_s, Config.cijoe(project_path).pass.to_s
|
74
|
+
if user != '' && pass != ''
|
75
|
+
use Rack::Auth::Basic do |username, password|
|
76
|
+
[ username, password ] == [ user, pass ]
|
77
|
+
end
|
78
|
+
puts "Using HTTP basic auth"
|
79
|
+
end
|
80
|
+
set :project_path, Proc.new{project_path}
|
81
|
+
end
|
82
|
+
|
83
|
+
def check_project
|
84
|
+
if options.project_path.nil? || !File.exists?(File.expand_path(options.project_path))
|
85
|
+
puts "Whoops! I need the path to a Git repo."
|
86
|
+
puts " $ git clone git@github.com:username/project.git project"
|
87
|
+
abort " $ cijoe project"
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<link href="<%= cijoe_root %>/screen.css" media="screen" rel="stylesheet" type="text/css" />
|
5
|
+
<link rel="shortcut icon" href="<%= cijoe_root %>/favicon.ico" type="image/x-icon" />
|
6
|
+
<title><%= h(joe.project) %>: CI Joe</title>
|
7
|
+
</head>
|
8
|
+
<body>
|
9
|
+
<div class="site">
|
10
|
+
<div class="title">
|
11
|
+
<a href="<%= cijoe_root %>/">CI Joe</a>
|
12
|
+
<span class="extra">because knowing is half the battle</span>
|
13
|
+
</div>
|
14
|
+
|
15
|
+
<div id="home">
|
16
|
+
<h1><a href="<%= joe.url %>"><%= joe.project %></a></h1>
|
17
|
+
<ul class="posts">
|
18
|
+
<% if joe.current_build %>
|
19
|
+
<li>
|
20
|
+
<span class="date"><%= pretty_time(joe.current_build.started_at) if joe.current_build %></span> »
|
21
|
+
<% if joe.current_build.sha %>
|
22
|
+
Building <a href="<%= joe.current_build.commit.url %>"><%= joe.current_build.short_sha %></a> <small>(pid: <%= joe.pid %>)</small>
|
23
|
+
<% else %>
|
24
|
+
Build starting...
|
25
|
+
<% end %>
|
26
|
+
</li>
|
27
|
+
<% else %>
|
28
|
+
<li><form method="POST"><input type="submit" value="Build"/></form></li>
|
29
|
+
<% end %>
|
30
|
+
|
31
|
+
<% if joe.last_build %>
|
32
|
+
<li>
|
33
|
+
<span class="date"><%= pretty_time(joe.last_build.finished_at) %></span> »
|
34
|
+
<% if joe.last_build.sha %>
|
35
|
+
Built <a href="<%= joe.last_build.commit.url %>"><%= joe.last_build.short_sha %></a>
|
36
|
+
<% end %>
|
37
|
+
<span class="<%= joe.last_build.status %>">(<%= joe.last_build.status %>)</span>
|
38
|
+
<% if joe.last_build.duration %>
|
39
|
+
in <span class="duration"><%= joe.last_build.duration %></span> seconds.
|
40
|
+
<% end %>
|
41
|
+
</li>
|
42
|
+
<% if joe.last_build.failed? %>
|
43
|
+
<li><pre class="terminal"><code><%=ansi_color_codes h(joe.last_build.output) %></code></pre></li>
|
44
|
+
<% end %>
|
45
|
+
<% end %>
|
46
|
+
</ul>
|
47
|
+
</div>
|
48
|
+
|
49
|
+
<div class="footer">
|
50
|
+
<div class="contact">
|
51
|
+
<p>
|
52
|
+
<a href="http://github.com/defunkt/cijoe/tree/master#readme">Documentation</a><br/>
|
53
|
+
<a href="http://github.com/defunkt/cijoe">Source</a><br/>
|
54
|
+
<a href="http://github.com/defunkt/cijoe/issues">Issues</a><br/>
|
55
|
+
<a href="http://twitter.com/defunkt">Twitter</a>
|
56
|
+
</p>
|
57
|
+
</div>
|
58
|
+
<div class="contact">
|
59
|
+
<p>
|
60
|
+
Designed by <a href="http://tom.preston-werner.com/">Tom Preston-Werner</a><br/>
|
61
|
+
Influenced by <a href="http://integrityapp.com/">Integrity</a><br/>
|
62
|
+
Built with <a href="http://sinatrarb.com/">Sinatra</a><br/>
|
63
|
+
Keep it simple, Sam.
|
64
|
+
</p>
|
65
|
+
</div>
|
66
|
+
<div class="rss">
|
67
|
+
<a href="http://github.com/defunkt/cijoe">
|
68
|
+
<img src="<%= cijoe_root %>/octocat.png" alt="Octocat!" />
|
69
|
+
</a>
|
70
|
+
</div>
|
71
|
+
</div>
|
72
|
+
</div>
|
73
|
+
</body>
|
74
|
+
</html>
|
data/lib/cijoe.rb
ADDED
@@ -0,0 +1,232 @@
|
|
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
|
+
write_build 'current', build
|
131
|
+
|
132
|
+
open_pipe("cd #{@project_path} && #{runner_command} 2>&1") do |pipe, pid|
|
133
|
+
puts "#{Time.now.to_i}: Building #{build.short_sha}: pid=#{pid}"
|
134
|
+
|
135
|
+
build.pid = pid
|
136
|
+
write_build 'current', build
|
137
|
+
output = pipe.read
|
138
|
+
end
|
139
|
+
|
140
|
+
Process.waitpid(build.pid)
|
141
|
+
status = $?.exitstatus.to_i
|
142
|
+
puts "#{Time.now.to_i}: Built #{build.short_sha}: status=#{status}"
|
143
|
+
|
144
|
+
status == 0 ? build_worked(output) : build_failed('', output)
|
145
|
+
rescue Object => e
|
146
|
+
puts "Exception building: #{e.message} (#{e.class})"
|
147
|
+
build_failed('', e.to_s)
|
148
|
+
end
|
149
|
+
|
150
|
+
# shellin' out
|
151
|
+
def runner_command
|
152
|
+
runner = repo_config.runner.to_s
|
153
|
+
runner == '' ? "rake -s test:units" : runner
|
154
|
+
end
|
155
|
+
|
156
|
+
def git_sha
|
157
|
+
`cd #{@project_path} && git rev-parse origin/#{git_branch}`.chomp
|
158
|
+
end
|
159
|
+
|
160
|
+
def git_update
|
161
|
+
`cd #{@project_path} && git fetch origin && git reset --hard origin/#{git_branch}`
|
162
|
+
run_hook "after-reset"
|
163
|
+
end
|
164
|
+
|
165
|
+
def git_user_and_project
|
166
|
+
Config.remote(@project_path).origin.url.to_s.chomp('.git').split(':')[-1].split('/')[-2, 2]
|
167
|
+
end
|
168
|
+
|
169
|
+
def git_branch
|
170
|
+
return @git_branch if @git_branch
|
171
|
+
branch = repo_config.branch.to_s
|
172
|
+
@git_branch = branch == '' ? "master" : branch
|
173
|
+
end
|
174
|
+
|
175
|
+
# massage our repo
|
176
|
+
def run_hook(hook)
|
177
|
+
if File.exists?(file=path_in_project(".git/hooks/#{hook}")) && File.executable?(file)
|
178
|
+
data =
|
179
|
+
if @last_build && @last_build.commit
|
180
|
+
{
|
181
|
+
"MESSAGE" => @last_build.commit.message,
|
182
|
+
"AUTHOR" => @last_build.commit.author,
|
183
|
+
"SHA" => @last_build.commit.sha,
|
184
|
+
"OUTPUT" => @last_build.env_output
|
185
|
+
}
|
186
|
+
else
|
187
|
+
{}
|
188
|
+
end
|
189
|
+
|
190
|
+
data.each{ |k, v| ENV[k] = v }
|
191
|
+
ret = `cd #{@project_path} && sh #{file}`
|
192
|
+
data.each{ |k, v| ENV[k] = nil }
|
193
|
+
ret
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
# restore current / last build state from disk.
|
198
|
+
def restore
|
199
|
+
@last_build = read_build('last')
|
200
|
+
@current_build = read_build('current')
|
201
|
+
|
202
|
+
Process.kill(0, @current_build.pid) if @current_build && @current_build.pid
|
203
|
+
rescue Errno::ESRCH
|
204
|
+
# build pid isn't running anymore. assume previous
|
205
|
+
# server died and reset.
|
206
|
+
@current_build = nil
|
207
|
+
end
|
208
|
+
|
209
|
+
def path_in_project(path)
|
210
|
+
File.join(@project_path, path)
|
211
|
+
end
|
212
|
+
|
213
|
+
# write build info for build to file.
|
214
|
+
def write_build(name, build)
|
215
|
+
filename = path_in_project(".git/builds/#{name}")
|
216
|
+
Dir.mkdir path_in_project('.git/builds') unless File.directory?(path_in_project('.git/builds'))
|
217
|
+
if build
|
218
|
+
build.dump filename
|
219
|
+
elsif File.exist?(filename)
|
220
|
+
File.unlink filename
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
def repo_config
|
225
|
+
Config.cijoe(@project_path)
|
226
|
+
end
|
227
|
+
|
228
|
+
# load build info from file.
|
229
|
+
def read_build(name)
|
230
|
+
Build.load(path_in_project(".git/builds/#{name}"), @project_path)
|
231
|
+
end
|
232
|
+
end
|
data/test/helper.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'test/unit'
|
3
|
+
|
4
|
+
ENV['RACK_ENV'] = 'test'
|
5
|
+
|
6
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
7
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
8
|
+
require 'cijoe'
|
9
|
+
|
10
|
+
CIJoe::Server.set :project_path, "."
|
11
|
+
CIJoe::Server.set :environment, "test"
|
12
|
+
|
13
|
+
class Test::Unit::TestCase
|
14
|
+
end
|
data/test/test_cijoe.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
class TestCIJoe < Test::Unit::TestCase
|
4
|
+
def test_raise_error_on_invalid_command
|
5
|
+
assert_raise RuntimeError, LoadError do
|
6
|
+
CIJoe::Config.new('--invalid').to_s
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
def test_return_value_of_config
|
11
|
+
assert_equal `git config blame`.chomp, CIJoe::Config.new('blame').to_s
|
12
|
+
end
|
13
|
+
|
14
|
+
def test_return_empty_string_when_config_does_not_exist
|
15
|
+
assert_equal '', CIJoe::Config.new('invalid').to_s
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
require "helper"
|
2
|
+
require "rack/test"
|
3
|
+
require "cijoe/server"
|
4
|
+
|
5
|
+
class TestCIJoeServer < Test::Unit::TestCase
|
6
|
+
include Rack::Test::Methods
|
7
|
+
|
8
|
+
class ::CIJoe
|
9
|
+
attr_writer :current_build, :last_build
|
10
|
+
end
|
11
|
+
|
12
|
+
attr_accessor :app
|
13
|
+
|
14
|
+
def setup
|
15
|
+
@app = CIJoe::Server.new
|
16
|
+
# make Build#restore a no-op so we don't overwrite our current/last
|
17
|
+
# build attributes set from tests.
|
18
|
+
joe = @app.joe
|
19
|
+
def joe.restore
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def test_ping
|
24
|
+
app.joe.last_build = build :worked
|
25
|
+
assert !app.joe.building?, "have a last build, but not a current"
|
26
|
+
|
27
|
+
get "/ping"
|
28
|
+
assert_equal 200, last_response.status
|
29
|
+
assert_equal app.joe.last_build.sha, last_response.body
|
30
|
+
end
|
31
|
+
|
32
|
+
def test_ping_building
|
33
|
+
app.joe.current_build = build :building
|
34
|
+
assert app.joe.building?, "buildin' a awsum project"
|
35
|
+
|
36
|
+
get "/ping"
|
37
|
+
assert_equal 412, last_response.status
|
38
|
+
assert_equal "building", last_response.body
|
39
|
+
end
|
40
|
+
|
41
|
+
def test_ping_building_with_a_previous_build
|
42
|
+
app.joe.last_build = build :worked
|
43
|
+
app.joe.current_build = build :building
|
44
|
+
assert app.joe.building?, "buildin' a awsum project"
|
45
|
+
|
46
|
+
get "/ping"
|
47
|
+
assert_equal 412, last_response.status
|
48
|
+
assert_equal "building", last_response.body
|
49
|
+
end
|
50
|
+
|
51
|
+
def test_ping_failed
|
52
|
+
app.joe.last_build = build :failed
|
53
|
+
|
54
|
+
get "/ping"
|
55
|
+
assert_equal 412, last_response.status
|
56
|
+
assert_equal app.joe.last_build.sha, last_response.body
|
57
|
+
end
|
58
|
+
|
59
|
+
def test_ping_should_not_reset_current_build_in_tests
|
60
|
+
current_build = build :building
|
61
|
+
app.joe.current_build = current_build
|
62
|
+
assert app.joe.building?
|
63
|
+
get "/ping"
|
64
|
+
assert_equal current_build, app.joe.current_build
|
65
|
+
end
|
66
|
+
|
67
|
+
# Create a new, fake build. All we care about is status.
|
68
|
+
|
69
|
+
def build status
|
70
|
+
CIJoe::Build.new "path", "user", "project", Time.now, Time.now,
|
71
|
+
"deadbeef", status, "output", nil
|
72
|
+
end
|
73
|
+
end
|
metadata
ADDED
@@ -0,0 +1,136 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: nyc-ruby-meetup_cijoe
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 5
|
8
|
+
- 1
|
9
|
+
version: 0.5.1
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- Chris Wanstrath
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2010-12-10 00:00:00 -05:00
|
18
|
+
default_executable: cicobra
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: rspec
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
none: false
|
25
|
+
requirements:
|
26
|
+
- - ">="
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
segments:
|
29
|
+
- 1
|
30
|
+
- 2
|
31
|
+
- 9
|
32
|
+
version: 1.2.9
|
33
|
+
type: :development
|
34
|
+
version_requirements: *id001
|
35
|
+
- !ruby/object:Gem::Dependency
|
36
|
+
name: yard
|
37
|
+
prerelease: false
|
38
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ">="
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
segments:
|
44
|
+
- 0
|
45
|
+
version: "0"
|
46
|
+
type: :development
|
47
|
+
version_requirements: *id002
|
48
|
+
- !ruby/object:Gem::Dependency
|
49
|
+
name: unicorn
|
50
|
+
prerelease: false
|
51
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
52
|
+
none: false
|
53
|
+
requirements:
|
54
|
+
- - ">="
|
55
|
+
- !ruby/object:Gem::Version
|
56
|
+
segments:
|
57
|
+
- 0
|
58
|
+
version: "0"
|
59
|
+
type: :runtime
|
60
|
+
version_requirements: *id003
|
61
|
+
- !ruby/object:Gem::Dependency
|
62
|
+
name: sinatra
|
63
|
+
prerelease: false
|
64
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ">="
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
segments:
|
70
|
+
- 0
|
71
|
+
version: "0"
|
72
|
+
type: :runtime
|
73
|
+
version_requirements: *id004
|
74
|
+
description: An extension of the cijoe project now more descriptive
|
75
|
+
email: mjording@opengotham.com
|
76
|
+
executables:
|
77
|
+
- cicobra
|
78
|
+
extensions: []
|
79
|
+
|
80
|
+
extra_rdoc_files:
|
81
|
+
- LICENSE
|
82
|
+
- README.md
|
83
|
+
files:
|
84
|
+
- lib/cijoe.rb
|
85
|
+
- lib/cijoe/build.rb
|
86
|
+
- lib/cijoe/campfire.rb
|
87
|
+
- lib/cijoe/commit.rb
|
88
|
+
- lib/cijoe/config.rb
|
89
|
+
- lib/cijoe/public/favicon.ico
|
90
|
+
- lib/cijoe/public/octocat.png
|
91
|
+
- lib/cijoe/public/screen.css
|
92
|
+
- lib/cijoe/server.rb
|
93
|
+
- lib/cijoe/version.rb
|
94
|
+
- lib/cijoe/views/template.erb
|
95
|
+
- LICENSE
|
96
|
+
- README.md
|
97
|
+
- test/helper.rb
|
98
|
+
- test/test_cijoe.rb
|
99
|
+
- test/test_cijoe_server.rb
|
100
|
+
- bin/cicobra
|
101
|
+
has_rdoc: true
|
102
|
+
homepage: http://github.com/nyc-ruby-meetup/cijoe
|
103
|
+
licenses: []
|
104
|
+
|
105
|
+
post_install_message:
|
106
|
+
rdoc_options: []
|
107
|
+
|
108
|
+
require_paths:
|
109
|
+
- lib
|
110
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
111
|
+
none: false
|
112
|
+
requirements:
|
113
|
+
- - ">="
|
114
|
+
- !ruby/object:Gem::Version
|
115
|
+
segments:
|
116
|
+
- 0
|
117
|
+
version: "0"
|
118
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
119
|
+
none: false
|
120
|
+
requirements:
|
121
|
+
- - ">="
|
122
|
+
- !ruby/object:Gem::Version
|
123
|
+
segments:
|
124
|
+
- 0
|
125
|
+
version: "0"
|
126
|
+
requirements: []
|
127
|
+
|
128
|
+
rubyforge_project:
|
129
|
+
rubygems_version: 1.3.7
|
130
|
+
signing_key:
|
131
|
+
specification_version: 3
|
132
|
+
summary: An extension of the cijoe project
|
133
|
+
test_files:
|
134
|
+
- test/helper.rb
|
135
|
+
- test/test_cijoe.rb
|
136
|
+
- test/test_cijoe_server.rb
|