ssp-cijoe 0.4.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.
@@ -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