nyc-ruby-meetup_cijoe 0.5.1

Sign up to get free protection for your applications and to get access to all the features.
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
+ [![The Battle](http://img.skitch.com/20090805-g4a2qhttwij8n2jr9t552efn3k.png)](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
+ ![Post-Receive URL](http://img.skitch.com/20090806-d2bxrk733gqu8m11tf4kyir5d8.png)
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
+ ![Building](http://img.skitch.com/20090806-ryw34ksi5ixnrdwxcptqy28iy7.png)
161
+
162
+ ![Built](http://img.skitch.com/20090806-f7j3r65yecaq13hdcxqwtc5krd.)
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]))
@@ -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
@@ -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
@@ -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
+
@@ -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,3 @@
1
+ class CIJoe
2
+ Version = VERSION = "0.5.0"
3
+ 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> &raquo;
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> &raquo;
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
@@ -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