felixclack-cijoe 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/LICENSE +20 -0
- data/README.md +175 -0
- data/Rakefile +31 -0
- data/bin/cijoe +50 -0
- data/lib/cijoe.rb +233 -0
- data/lib/cijoe/build.rb +67 -0
- data/lib/cijoe/campfire.rb +78 -0
- data/lib/cijoe/commit.rb +27 -0
- data/lib/cijoe/config.rb +43 -0
- data/lib/cijoe/public/favicon.ico +0 -0
- data/lib/cijoe/public/octocat.png +0 -0
- data/lib/cijoe/public/screen.css +222 -0
- data/lib/cijoe/server.rb +115 -0
- data/lib/cijoe/version.rb +3 -0
- data/lib/cijoe/views/json.erb +8 -0
- data/lib/cijoe/views/template.erb +43 -0
- data/test/helper.rb +14 -0
- data/test/test_cijoe.rb +17 -0
- data/test/test_cijoe_server.rb +93 -0
- metadata +126 -0
data/lib/cijoe/build.rb
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
class CIJoe
|
4
|
+
class Build < Struct.new(:project_path, :user, :project, :started_at, :finished_at, :sha, :status, :output, :pid, :branch)
|
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, branch]
|
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,78 @@
|
|
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 "\ttoken = your_api_token"
|
19
|
+
puts "\tsubdomain = whatever"
|
20
|
+
puts "\troom = Awesomeness"
|
21
|
+
puts "\tssl = false"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.config
|
26
|
+
campfire_config = Config.new('campfire', @project_path)
|
27
|
+
@config ||= {
|
28
|
+
:subdomain => campfire_config.subdomain.to_s,
|
29
|
+
:token => campfire_config.token.to_s,
|
30
|
+
:room => campfire_config.room.to_s,
|
31
|
+
:ssl => campfire_config.ssl.to_s.strip == 'true'
|
32
|
+
}
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.valid_config?
|
36
|
+
%w( subdomain token room ).all? do |key|
|
37
|
+
!config[key.intern].empty?
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def notify
|
42
|
+
room.speak "#{short_message}. #{commit.url}"
|
43
|
+
room.play "#{play_sound}"
|
44
|
+
room.paste full_message if failed?
|
45
|
+
room.leave
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
def room
|
50
|
+
@room ||= begin
|
51
|
+
config = Campfire.config
|
52
|
+
campfire = Tinder::Campfire.new(config[:subdomain],
|
53
|
+
:token => config[:token],
|
54
|
+
:ssl => config[:ssl] || false)
|
55
|
+
campfire.find_room_by_name(config[:room])
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def short_message
|
60
|
+
"#{branch} at #{short_sha} of #{project} " +
|
61
|
+
(worked? ? "passed" : "failed") + " (#{duration.to_i}s)"
|
62
|
+
end
|
63
|
+
|
64
|
+
def play_sound
|
65
|
+
"#{worked? ? "rimshot" : "trombone"}"
|
66
|
+
end
|
67
|
+
|
68
|
+
def full_message
|
69
|
+
<<-EOM
|
70
|
+
Commit Message: #{commit.message}
|
71
|
+
Commit Date: #{commit.committed_at}
|
72
|
+
Commit Author: #{commit.author}
|
73
|
+
|
74
|
+
#{clean_output}
|
75
|
+
EOM
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
data/lib/cijoe/commit.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
class CIJoe
|
2
|
+
class Commit < Struct.new(:sha, :user, :project, :project_path)
|
3
|
+
def url
|
4
|
+
"http://github.com/#{user}/#{project}/commit/#{sha}"
|
5
|
+
end
|
6
|
+
|
7
|
+
def author
|
8
|
+
raw_commit_lines.grep(/Author:/).first.split(':', 2)[-1]
|
9
|
+
end
|
10
|
+
|
11
|
+
def committed_at
|
12
|
+
raw_commit_lines.grep(/Date:/).first.split(':', 2)[-1]
|
13
|
+
end
|
14
|
+
|
15
|
+
def message
|
16
|
+
raw_commit.split("\n\n", 3)[1].to_s.strip
|
17
|
+
end
|
18
|
+
|
19
|
+
def raw_commit
|
20
|
+
@raw_commit ||= `cd #{project_path} && git show #{sha}`.chomp
|
21
|
+
end
|
22
|
+
|
23
|
+
def raw_commit_lines
|
24
|
+
@raw_commit_lines ||= raw_commit.split("\n")
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
data/lib/cijoe/config.rb
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
class CIJoe
|
2
|
+
class Config
|
3
|
+
def self.method_missing(command, *args)
|
4
|
+
new(command, *args)
|
5
|
+
end
|
6
|
+
|
7
|
+
def initialize(command, project_path = nil, parent = nil)
|
8
|
+
@command = command
|
9
|
+
@parent = parent
|
10
|
+
@project_path = project_path || File.join(File.dirname(__FILE__), '../../')
|
11
|
+
end
|
12
|
+
|
13
|
+
def method_missing(command, *args)
|
14
|
+
Config.new(command, @project_path, self)
|
15
|
+
end
|
16
|
+
|
17
|
+
def to_s
|
18
|
+
git_command = "cd #{@project_path} && git config #{config_string}"
|
19
|
+
result = `#{git_command} 2>&1`.chomp
|
20
|
+
process_status = $?
|
21
|
+
|
22
|
+
if successful_command?(process_status) || config_command_with_empty_value?(result,process_status)
|
23
|
+
return result
|
24
|
+
else
|
25
|
+
raise "Error calling git config, is a recent version of git installed? Command: #{git_command.inspect}, Error: #{result.inspect}, Status: #{process_status.inspect}"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def config_string
|
30
|
+
@parent ? "#{@parent.config_string}.#{@command}" : @command
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def successful_command?(process_status)
|
36
|
+
process_status.exitstatus.to_i == 0
|
37
|
+
end
|
38
|
+
|
39
|
+
def config_command_with_empty_value?(result, process_status)
|
40
|
+
process_status.exitstatus.to_i == 1 && result.empty?
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
Binary file
|
Binary file
|
@@ -0,0 +1,222 @@
|
|
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: 4px;
|
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: #c4b430 !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: #00a;
|
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: 100%;
|
103
|
+
line-height: 1.5em;
|
104
|
+
}
|
105
|
+
|
106
|
+
.title {
|
107
|
+
color: #a00;
|
108
|
+
font-weight: bold;
|
109
|
+
margin-bottom: 2em;
|
110
|
+
}
|
111
|
+
|
112
|
+
.site .title a {
|
113
|
+
color: #a00;
|
114
|
+
text-decoration: none;
|
115
|
+
}
|
116
|
+
|
117
|
+
.site .title a:hover {
|
118
|
+
color: black;
|
119
|
+
}
|
120
|
+
|
121
|
+
.site .title .extra {
|
122
|
+
color: #aaa;
|
123
|
+
text-decoration: none;
|
124
|
+
margin-left: 1em;
|
125
|
+
font-size: 0.9em;
|
126
|
+
}
|
127
|
+
|
128
|
+
.site .title a.extra:hover {
|
129
|
+
color: black;
|
130
|
+
}
|
131
|
+
|
132
|
+
.site .meta {
|
133
|
+
color: #aaa;
|
134
|
+
}
|
135
|
+
|
136
|
+
.site .footer {
|
137
|
+
font-size: 80%;
|
138
|
+
color: #666;
|
139
|
+
border-top: 4px solid #eee;
|
140
|
+
margin-top: 2em;
|
141
|
+
overflow: hidden;
|
142
|
+
}
|
143
|
+
|
144
|
+
.site .footer .contact {
|
145
|
+
float: left;
|
146
|
+
margin-right: 3em;
|
147
|
+
}
|
148
|
+
|
149
|
+
.site .footer .contact a {
|
150
|
+
color: #8085C1;
|
151
|
+
}
|
152
|
+
|
153
|
+
.site .footer .rss {
|
154
|
+
margin-top: 1.1em;
|
155
|
+
margin-right: -.2em;
|
156
|
+
float: right;
|
157
|
+
}
|
158
|
+
|
159
|
+
.site .footer .rss img {
|
160
|
+
border: 0;
|
161
|
+
}
|
162
|
+
|
163
|
+
input {
|
164
|
+
float: right;
|
165
|
+
margin-right:5px;
|
166
|
+
}
|
167
|
+
/*****************************************************************************/
|
168
|
+
/*
|
169
|
+
/* Posts
|
170
|
+
/*
|
171
|
+
/*****************************************************************************/
|
172
|
+
|
173
|
+
#post {
|
174
|
+
|
175
|
+
}
|
176
|
+
|
177
|
+
/* standard */
|
178
|
+
|
179
|
+
#post pre {
|
180
|
+
border: 2px solid #ddd;
|
181
|
+
background-color: #333;
|
182
|
+
padding: 0 .4em;
|
183
|
+
}
|
184
|
+
|
185
|
+
#post ul,
|
186
|
+
#post ol {
|
187
|
+
margin-left: 1.25em;
|
188
|
+
}
|
189
|
+
|
190
|
+
#post code {
|
191
|
+
border: 1px solid #ddd;
|
192
|
+
background-color: #eef;
|
193
|
+
font-size: 95%;
|
194
|
+
padding: 0 .2em;
|
195
|
+
}
|
196
|
+
|
197
|
+
#post pre code {
|
198
|
+
border: none;
|
199
|
+
}
|
200
|
+
|
201
|
+
/* terminal */
|
202
|
+
|
203
|
+
pre.terminal {
|
204
|
+
border: 2px solid #999;
|
205
|
+
background-color: #fff;
|
206
|
+
color: #333;
|
207
|
+
padding: 15px;
|
208
|
+
/* overflow: auto; */
|
209
|
+
word-wrap: break-word;
|
210
|
+
}
|
211
|
+
|
212
|
+
pre.terminal code {
|
213
|
+
font-family: 'Bitstream Vera Sans Mono', 'Courier', monospace;
|
214
|
+
background-color: #fff;
|
215
|
+
}
|
216
|
+
|
217
|
+
pre {
|
218
|
+
white-space: pre-wrap; /* CSS2.1 compliant */
|
219
|
+
white-space: -moz-pre-wrap; /* Mozilla-based browsers */
|
220
|
+
white-space: o-pre-wrap; /* Opera 7+ */
|
221
|
+
}
|
222
|
+
|
data/lib/cijoe/server.rb
ADDED
@@ -0,0 +1,115 @@
|
|
1
|
+
require 'sinatra/base'
|
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 =~ /"ref":"(.+?)"/
|
32
|
+
pushed_branch = $1.split('/').last
|
33
|
+
end
|
34
|
+
|
35
|
+
# Only build if we were given an explicit branch via `?branch=blah`,
|
36
|
+
# no payload exists (we're probably testing), or the payload exists and
|
37
|
+
# the "ref" property matches our specified build branch.
|
38
|
+
if params[:branch] || payload.empty? || pushed_branch == joe.git_branch
|
39
|
+
joe.build(params[:branch])
|
40
|
+
end
|
41
|
+
|
42
|
+
redirect request.path
|
43
|
+
end
|
44
|
+
|
45
|
+
get '/api/json' do
|
46
|
+
response = [200, {'Content-Type' => 'application/json'}]
|
47
|
+
response_json = erb(:json, {}, :joe => joe)
|
48
|
+
if params[:jsonp]
|
49
|
+
response << params[:jsonp] + '(' + response_json + ')'
|
50
|
+
else
|
51
|
+
response << response_json
|
52
|
+
end
|
53
|
+
response
|
54
|
+
end
|
55
|
+
|
56
|
+
|
57
|
+
helpers do
|
58
|
+
include Rack::Utils
|
59
|
+
alias_method :h, :escape_html
|
60
|
+
|
61
|
+
# thanks integrity!
|
62
|
+
def ansi_color_codes(string)
|
63
|
+
string.gsub("\e[0m", '</span>').
|
64
|
+
gsub(/\e\[(\d+)m/, "<span class=\"color\\1\">")
|
65
|
+
end
|
66
|
+
|
67
|
+
def pretty_time(time)
|
68
|
+
time.strftime("%Y-%m-%d %H:%M")
|
69
|
+
end
|
70
|
+
|
71
|
+
def cijoe_root
|
72
|
+
root = request.path
|
73
|
+
root = "" if root == "/"
|
74
|
+
root
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def initialize(*args)
|
79
|
+
super
|
80
|
+
check_project
|
81
|
+
@joe = CIJoe.new(options.project_path)
|
82
|
+
|
83
|
+
CIJoe::Campfire.activate(options.project_path)
|
84
|
+
end
|
85
|
+
|
86
|
+
def self.start(host, port, project_path)
|
87
|
+
set :project_path, project_path
|
88
|
+
CIJoe::Server.run! :host => host, :port => port
|
89
|
+
end
|
90
|
+
|
91
|
+
def self.rack_start(project_path)
|
92
|
+
set :project_path, project_path
|
93
|
+
self.new
|
94
|
+
end
|
95
|
+
|
96
|
+
def self.project_path=(project_path)
|
97
|
+
user, pass = Config.cijoe(project_path).user.to_s, Config.cijoe(project_path).pass.to_s
|
98
|
+
if user != '' && pass != ''
|
99
|
+
use Rack::Auth::Basic do |username, password|
|
100
|
+
[ username, password ] == [ user, pass ]
|
101
|
+
end
|
102
|
+
puts "Using HTTP basic auth"
|
103
|
+
end
|
104
|
+
set :project_path, Proc.new{project_path}
|
105
|
+
end
|
106
|
+
|
107
|
+
def check_project
|
108
|
+
if options.project_path.nil? || !File.exists?(File.expand_path(options.project_path))
|
109
|
+
puts "Whoops! I need the path to a Git repo."
|
110
|
+
puts " $ git clone git@github.com:username/project.git project"
|
111
|
+
abort " $ cijoe project"
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|