ssp-cijoe 0.4.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,90 @@
1
+ require 'sinatra/base'
2
+ require 'erb'
3
+ require 'cijoe/partials'
4
+
5
+ class CIJoe
6
+ class Server < Sinatra::Base
7
+ attr_reader :joe
8
+
9
+ helpers Sinatra::Partials
10
+
11
+ dir = File.dirname(File.expand_path(__FILE__))
12
+
13
+ set :views, "#{dir}/views"
14
+ set :public, "#{dir}/public"
15
+ set :static, true
16
+ set :lock, true
17
+
18
+ before { joe.restore }
19
+
20
+ get '/ping' do
21
+ if joe.building? || !joe.last_build || !joe.last_build.worked?
22
+ halt 412, joe.last_build ? joe.last_build.sha : "building"
23
+ end
24
+
25
+ joe.last_build.sha
26
+ end
27
+
28
+ get '/?' do
29
+ erb(:template, {}, :joe => joe)
30
+ end
31
+
32
+ post '/?' do
33
+ payload = params[:payload].to_s
34
+ if payload.empty? || payload.include?(joe.git_branch)
35
+ joe.build
36
+ end
37
+ redirect request.path
38
+ end
39
+
40
+ user, pass = Config.cijoe.user.to_s, Config.cijoe.pass.to_s
41
+ if user != '' && pass != ''
42
+ use Rack::Auth::Basic do |username, password|
43
+ [ username, password ] == [ user, pass ]
44
+ end
45
+ puts "Using HTTP basic auth"
46
+ end
47
+
48
+ helpers do
49
+ include Rack::Utils
50
+ alias_method :h, :escape_html
51
+
52
+ # thanks integrity!
53
+ def ansi_color_codes(string)
54
+ string.gsub("\e[0m", '</span>').
55
+ gsub(/\e\[(\d+)m/, "<span class=\"color\\1\">")
56
+ end
57
+
58
+ def pretty_time(time)
59
+ time.strftime("%Y-%m-%d %H:%M")
60
+ end
61
+
62
+ def cijoe_root
63
+ root = request.path
64
+ root = "" if root == "/"
65
+ root
66
+ end
67
+ end
68
+
69
+ def initialize(*args)
70
+ super
71
+ check_project
72
+ @joe = CIJoe.new(options.project_path)
73
+
74
+ CIJoe::Campfire.activate
75
+ end
76
+
77
+ def self.start(host, port, project_path)
78
+ set :project_path, project_path
79
+ CIJoe::Server.run! :host => host, :port => port
80
+ end
81
+
82
+ def check_project
83
+ if options.project_path.nil? || !File.exists?(File.expand_path(options.project_path))
84
+ puts "Whoops! I need the path to a Git repo."
85
+ puts " $ git clone git@github.com:username/project.git project"
86
+ abort " $ cijoe project"
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,3 @@
1
+ class CIJoe
2
+ Version = "0.4.1"
3
+ end
@@ -0,0 +1,12 @@
1
+ <li>
2
+ <span class="date"><%= pretty_time(build.finished_at) %></span> &raquo; Built <a href="<%= build.commit.url %>"><%= build.short_sha %></a> <span class="<%= build.status %>">(<%= build.status %>)</span>
3
+ <span class="metrics">
4
+ [ <a href="<%= build.metrics_url %>">Metrics</a> ]
5
+ </span>
6
+ <span class="coverage">
7
+ [ <a href="<%= build.coverage_url %>">Coverage</a> ]
8
+ </span>
9
+ <% if build.duration %>
10
+ in <span class="duration"><%= build.duration %></span> seconds.
11
+ <% end %>
12
+ </li>
@@ -0,0 +1,75 @@
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
+ <%= partial :build, :locals => {:build => joe.last_build} %>
33
+ <% if joe.last_build.failed? %>
34
+ <li><pre class="terminal"><code><%=ansi_color_codes h(joe.last_build.output) %></code></pre></li>
35
+ <% end %>
36
+ <% end %>
37
+ </ul>
38
+
39
+ <% unless joe.build_history.empty? %>
40
+ <h1><a href="<%= joe.url %>"><%= joe.project %></a> <span style="color: #999;">build history</span></h1>
41
+ <ul class="posts">
42
+ <% joe.build_history.each do |current_build| %>
43
+ <%= partial :build, :locals => {:build => current_build} %>
44
+ <% end %>
45
+ </ul>
46
+ <% end %>
47
+
48
+ </div>
49
+
50
+ <div class="footer">
51
+ <div class="contact">
52
+ <p>
53
+ <a href="http://github.com/defunkt/cijoe/tree/master#readme">Documentation</a><br/>
54
+ <a href="http://github.com/defunkt/cijoe">Source</a><br/>
55
+ <a href="http://github.com/defunkt/cijoe/issues">Issues</a><br/>
56
+ <a href="http://twitter.com/defunkt">Twitter</a>
57
+ </p>
58
+ </div>
59
+ <div class="contact">
60
+ <p>
61
+ Designed by <a href="http://tom.preston-werner.com/">Tom Preston-Werner</a><br/>
62
+ Influenced by <a href="http://integrityapp.com/">Integrity</a><br/>
63
+ Built with <a href="http://sinatrarb.com/">Sinatra</a><br/>
64
+ Keep it simple, Sam.
65
+ </p>
66
+ </div>
67
+ <div class="rss">
68
+ <a href="http://github.com/defunkt/cijoe">
69
+ <img src="<%= cijoe_root %>/octocat.png" alt="Octocat!" />
70
+ </a>
71
+ </div>
72
+ </div>
73
+ </div>
74
+ </body>
75
+ </html>
data/lib/cijoe.rb ADDED
@@ -0,0 +1,237 @@
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
+
25
+ attr_reader :user, :project, :url, :current_build, :last_build
26
+
27
+ def initialize(project_path)
28
+ project_path = File.expand_path(project_path)
29
+ Dir.chdir(project_path)
30
+
31
+ @user, @project = git_user_and_project
32
+ @url = "http://github.com/#{@user}/#{@project}"
33
+
34
+ @last_build = nil
35
+ @current_build = nil
36
+
37
+ trap("INT") { stop }
38
+ end
39
+
40
+ # is a build running?
41
+ def building?
42
+ !!@current_build
43
+ end
44
+
45
+ # the pid of the running child process
46
+ def pid
47
+ building? and current_build.pid
48
+ end
49
+
50
+ # kill the child and exit
51
+ def stop
52
+ # another build waits
53
+ if Config.cijoe.buildallfile && File.exist?(Config.cijoe.buildallfile.to_s)
54
+ # clean out on stop
55
+ FileUtils.rm(Config.cijoe.buildallfile.to_s)
56
+ end
57
+
58
+ Process.kill(9, pid) if pid
59
+ exit!
60
+ end
61
+
62
+ # build callbacks
63
+ def build_failed(output, error)
64
+ finish_build :failed, "#{error}\n\n#{output}"
65
+ run_hook "build-failed"
66
+ end
67
+
68
+ def build_worked(output)
69
+ finish_build :worked, output
70
+ run_hook "build-worked"
71
+ end
72
+
73
+ def finish_build(status, output)
74
+ @current_build.finished_at = Time.now
75
+ @current_build.status = status
76
+ @current_build.output = output
77
+ @last_build = @current_build
78
+
79
+ @current_build = nil
80
+
81
+ actual_build_id = "#{@last_build.commit.sha[0..7]}.#{Time.now.to_i}.build"
82
+ write_build actual_build_id, @last_build
83
+ write_build 'current', @current_build
84
+ write_build 'last', @last_build
85
+ @last_build.notify if @last_build.respond_to? :notify
86
+
87
+ # another build waits
88
+ if Config.cijoe.buildallfile && File.exist?(Config.cijoe.buildallfile.to_s)
89
+ # clean out before new build
90
+ FileUtils.rm(Config.cijoe.buildallfile.to_s)
91
+ build
92
+ end
93
+ end
94
+
95
+ # run the build but make sure only one is running
96
+ # at a time (if new one comes in we will park it)
97
+ def build
98
+ if building?
99
+ # only if switched on to build all incoming requests
100
+ if Config.cijoe.buildallfile
101
+ # and there is no previous request
102
+ return if File.exist?(Config.cijoe.buildallfile.to_s)
103
+ # we will mark awaiting builds
104
+ FileUtils.touch(Config.cijoe.buildallfile.to_s)
105
+ end
106
+ # leave anyway because a current build runs
107
+ return
108
+ end
109
+ @current_build = Build.new(@user, @project)
110
+ write_build 'current', @current_build
111
+ Thread.new { build! }
112
+ end
113
+
114
+ def open_pipe(cmd)
115
+ read, write = IO.pipe
116
+
117
+ pid = fork do
118
+ read.close
119
+ $stdout.reopen write
120
+ exec cmd
121
+ end
122
+
123
+ write.close
124
+
125
+ yield read, pid
126
+ end
127
+
128
+ # update git then run the build
129
+ def build!
130
+ build = @current_build
131
+ output = ''
132
+ git_update
133
+ build.sha = git_sha
134
+ write_build 'current', build
135
+
136
+ open_pipe("#{runner_command} 2>&1") do |pipe, pid|
137
+ puts "#{Time.now.to_i}: Building #{build.short_sha}: pid=#{pid}"
138
+
139
+ build.pid = pid
140
+ write_build 'current', build
141
+ output = pipe.read
142
+ end
143
+
144
+ Process.waitpid(build.pid)
145
+ status = $?.exitstatus.to_i
146
+ puts "#{Time.now.to_i}: Built #{build.short_sha}: status=#{status}"
147
+
148
+ status == 0 ? build_worked(output) : build_failed('', output)
149
+ rescue Object => e
150
+ puts "Exception building: #{e.message} (#{e.class})"
151
+ build_failed('', e.to_s)
152
+ end
153
+
154
+ # shellin' out
155
+ def runner_command
156
+ runner = Config.cijoe.runner.to_s
157
+ runner == '' ? "rake -s test:units" : runner
158
+ end
159
+
160
+ def git_sha
161
+ `git rev-parse origin/#{git_branch}`.chomp
162
+ end
163
+
164
+ def git_update
165
+ `git fetch origin && git reset --hard origin/#{git_branch}`
166
+ run_hook "after-reset"
167
+ end
168
+
169
+ def git_user_and_project
170
+ Config.remote.origin.url.to_s.chomp('.git').split(':')[-1].split('/')[-2, 2]
171
+ end
172
+
173
+ def git_branch
174
+ branch = Config.cijoe.branch.to_s
175
+ branch == '' ? "master" : branch
176
+ end
177
+
178
+ # massage our repo
179
+ def run_hook(hook)
180
+ if File.exists?(file=".git/hooks/#{hook}") && File.executable?(file)
181
+ data =
182
+ if @last_build && @last_build.commit
183
+ {
184
+ "MESSAGE" => @last_build.commit.message,
185
+ "AUTHOR" => @last_build.commit.author,
186
+ "SHA" => @last_build.commit.sha,
187
+ "OUTPUT" => @last_build.clean_output
188
+ }
189
+ else
190
+ {}
191
+ end
192
+ data.each{ |k, v| ENV[k] = v }
193
+ `sh #{file}`
194
+ end
195
+ end
196
+
197
+ # restore current / last build state from disk.
198
+ def restore
199
+ unless @last_build
200
+ @last_build = read_build('last')
201
+ end
202
+
203
+ unless @current_build
204
+ @current_build = read_build('current')
205
+ end
206
+
207
+ Process.kill(0, @current_build.pid) if @current_build && @current_build.pid
208
+ rescue Errno::ESRCH
209
+ # build pid isn't running anymore. assume previous
210
+ # server died and reset.
211
+ @current_build = nil
212
+ end
213
+
214
+ # write build info for build to file.
215
+ def write_build(name, build)
216
+ filename = ".git/builds/#{name}"
217
+ Dir.mkdir '.git/builds' unless File.directory?('.git/builds')
218
+ if build
219
+ build.dump filename
220
+ elsif File.exist?(filename)
221
+ File.unlink filename
222
+ end
223
+ end
224
+
225
+ # load build info from file.
226
+ def read_build(name)
227
+ Build.load(".git/builds/#{name}")
228
+ end
229
+
230
+ def build_history
231
+ build_entries = Dir.open(".git/builds").entries.select { |dir| dir.match /^([a-z0-9]{8})\.([\d]{10})\.build$/ }
232
+ history = build_entries.inject([]) do |builds, entry|
233
+ builds << read_build(entry)
234
+ end
235
+ history
236
+ end
237
+ end
data/test/helper.rb ADDED
@@ -0,0 +1,12 @@
1
+ require 'rubygems'
2
+ require 'test/unit'
3
+
4
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
5
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
6
+ require 'cijoe'
7
+
8
+ CIJoe::Server.set :project_path, "."
9
+ CIJoe::Server.set :environment, "test"
10
+
11
+ class Test::Unit::TestCase
12
+ 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,50 @@
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
+ end
17
+
18
+ def test_ping
19
+ app.joe.last_build = build :worked
20
+ assert !app.joe.building?, "have a last build, but not a current"
21
+
22
+ get "/ping"
23
+ assert_equal 200, last_response.status
24
+ assert_equal app.joe.last_build.sha, last_response.body
25
+ end
26
+
27
+ def test_ping_building
28
+ app.joe.current_build = build :building
29
+ assert app.joe.building?, "buildin' a awsum project"
30
+
31
+ get "/ping"
32
+ assert_equal 412, last_response.status
33
+ assert_equal "building", last_response.body
34
+ end
35
+
36
+ def test_ping_failed
37
+ app.joe.last_build = build :failed
38
+
39
+ get "/ping"
40
+ assert_equal 412, last_response.status
41
+ assert_equal app.joe.last_build.sha, last_response.body
42
+ end
43
+
44
+ # Create a new, fake build. All we care about is status.
45
+
46
+ def build status
47
+ CIJoe::Build.new "user", "project", Time.now, Time.now,
48
+ "deadbeef", status, "output", 1337
49
+ end
50
+ end