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 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