ciquantum 0.0.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/.gitignore +21 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +18 -0
- data/Rakefile +34 -0
- data/bin/ciquantum +50 -0
- data/ciquantum.gemspec +30 -0
- data/examples/build-failed +22 -0
- data/examples/build-worked +22 -0
- data/examples/cijoe.ru +15 -0
- data/examples/cijoed +53 -0
- data/lib/ciquantum.rb +227 -0
- data/lib/ciquantum/adapter.rb +11 -0
- data/lib/ciquantum/build.rb +67 -0
- data/lib/ciquantum/commit.rb +28 -0
- data/lib/ciquantum/config.rb +43 -0
- data/lib/ciquantum/public/favicon.ico +0 -0
- data/lib/ciquantum/public/octocat.png +0 -0
- data/lib/ciquantum/public/screen.css +213 -0
- data/lib/ciquantum/queue.rb +55 -0
- data/lib/ciquantum/server.rb +117 -0
- data/lib/ciquantum/unfuddle_adapter.rb +19 -0
- data/lib/ciquantum/version.rb +3 -0
- data/lib/ciquantum/views/json.erb +16 -0
- data/lib/ciquantum/views/template.erb +89 -0
- data/test/fixtures/payload.json +52 -0
- data/test/helper.rb +48 -0
- data/test/test_ciquantum_queue.rb +28 -0
- data/test/test_hooks.rb +81 -0
- metadata +209 -0
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
class CIQuantum
|
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,28 @@
|
|
1
|
+
class CIQuantum
|
2
|
+
class Commit < Struct.new(:sha, :user, :project, :project_path)
|
3
|
+
def url
|
4
|
+
Adapter::default_adapter.commit_url self
|
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
|
+
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
class CIQuantum
|
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
|
+
return ""
|
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,55 @@
|
|
1
|
+
class CIQuantum
|
2
|
+
# An in memory queue used for maintaining an order list of requested
|
3
|
+
# builds.
|
4
|
+
class Queue
|
5
|
+
# enabled - determines whether builds should be queued or not.
|
6
|
+
def initialize(enabled, verbose=false)
|
7
|
+
@enabled = enabled
|
8
|
+
@verbose = verbose
|
9
|
+
@queue = []
|
10
|
+
|
11
|
+
log("Build queueing enabled") if enabled
|
12
|
+
end
|
13
|
+
|
14
|
+
# Public: Appends a branch to be built, unless it already exists
|
15
|
+
# within the queue.
|
16
|
+
#
|
17
|
+
# branch - the name of the branch to build or nil if the default
|
18
|
+
# should be built.
|
19
|
+
#
|
20
|
+
# Returns nothing
|
21
|
+
def append_unless_already_exists(branch)
|
22
|
+
return unless enabled?
|
23
|
+
unless @queue.include? branch
|
24
|
+
@queue << branch
|
25
|
+
log "#{Time.now.to_i}: Queueing #{branch}"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# Returns a String of the next branch to build
|
30
|
+
def next_branch_to_build
|
31
|
+
branch = @queue.shift
|
32
|
+
log "#{Time.now.to_i}: De-queueing #{branch}"
|
33
|
+
branch
|
34
|
+
end
|
35
|
+
|
36
|
+
# Returns true if there are requested builds waiting and false
|
37
|
+
# otherwise.
|
38
|
+
def waiting?
|
39
|
+
if enabled?
|
40
|
+
not @queue.empty?
|
41
|
+
else
|
42
|
+
false
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
protected
|
47
|
+
def log(msg)
|
48
|
+
puts msg if @verbose
|
49
|
+
end
|
50
|
+
|
51
|
+
def enabled?
|
52
|
+
@enabled
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
require 'sinatra/base'
|
2
|
+
require 'erb'
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
class CIQuantum
|
6
|
+
class Server < Sinatra::Base
|
7
|
+
attr_reader :quantum
|
8
|
+
|
9
|
+
dir = File.dirname(File.expand_path(__FILE__))
|
10
|
+
|
11
|
+
set :views, "#{dir}/views"
|
12
|
+
set :public_folder, "#{dir}/public"
|
13
|
+
set :static, true
|
14
|
+
set :lock, true
|
15
|
+
|
16
|
+
before { quantum.restore }
|
17
|
+
|
18
|
+
get '/ping' do
|
19
|
+
if quantum.building? || !quantum.last_build || !quantum.last_build.worked?
|
20
|
+
halt 412, (quantum.building? || quantum.last_build.nil?) ? "building" : quantum.last_build.sha
|
21
|
+
end
|
22
|
+
|
23
|
+
quantum.last_build.sha
|
24
|
+
end
|
25
|
+
|
26
|
+
get '/?' do
|
27
|
+
erb(:template, {}, :quantum => quantum)
|
28
|
+
end
|
29
|
+
|
30
|
+
post '/?' do
|
31
|
+
unless params[:rebuild]
|
32
|
+
payload = JSON.parse(params[:payload])
|
33
|
+
pushed_branch = payload["ref"].split('/').last
|
34
|
+
end
|
35
|
+
|
36
|
+
# Only build if we were given an explicit branch via `?branch=blah`
|
37
|
+
# or the payload exists and the "ref" property matches our
|
38
|
+
# specified build branch.
|
39
|
+
if params[:branch] || params[:rebuild] || pushed_branch == quantum.git_branch
|
40
|
+
quantum.build(params[:branch])
|
41
|
+
end
|
42
|
+
|
43
|
+
redirect request.path
|
44
|
+
end
|
45
|
+
|
46
|
+
get '/api/json' do
|
47
|
+
response = [200, {'Content-Type' => 'application/json'}]
|
48
|
+
response_json = erb(:json, {}, :quantum => quantum)
|
49
|
+
if params[:jsonp]
|
50
|
+
response << params[:jsonp] + '(' + response_json + ')'
|
51
|
+
else
|
52
|
+
response << response_json
|
53
|
+
end
|
54
|
+
response
|
55
|
+
end
|
56
|
+
|
57
|
+
get '/coverage' do
|
58
|
+
redirect quantum.coverage_path
|
59
|
+
end
|
60
|
+
|
61
|
+
helpers do
|
62
|
+
include Rack::Utils
|
63
|
+
alias_method :h, :escape_html
|
64
|
+
|
65
|
+
# thanks integrity!
|
66
|
+
def ansi_color_codes(string)
|
67
|
+
string.gsub("\e[0m", '</span>').
|
68
|
+
gsub(/\e\[(\d+)m/, "<span class=\"color\\1\">")
|
69
|
+
end
|
70
|
+
|
71
|
+
def pretty_time(time)
|
72
|
+
time.strftime("%Y-%m-%d %H:%M")
|
73
|
+
end
|
74
|
+
|
75
|
+
def ciquantum_root
|
76
|
+
root = request.path
|
77
|
+
root = "" if root == "/"
|
78
|
+
root
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def initialize(*args)
|
83
|
+
super
|
84
|
+
check_project
|
85
|
+
@quantum = CIQuantum.new(settings.project_path)
|
86
|
+
end
|
87
|
+
|
88
|
+
def self.start(host, port, project_path)
|
89
|
+
set :project_path, project_path
|
90
|
+
CIQuantum::Server.run! :host => host, :port => port
|
91
|
+
end
|
92
|
+
|
93
|
+
def self.rack_start(project_path)
|
94
|
+
set :project_path, project_path
|
95
|
+
self.new
|
96
|
+
end
|
97
|
+
|
98
|
+
def self.project_path=(project_path)
|
99
|
+
user, pass = Config.ciquantum(project_path).user.to_s, Config.ciquantum(project_path).pass.to_s
|
100
|
+
if user != '' && pass != ''
|
101
|
+
use Rack::Auth::Basic do |username, password|
|
102
|
+
[ username, password ] == [ user, pass ]
|
103
|
+
end
|
104
|
+
puts "Using HTTP basic auth"
|
105
|
+
end
|
106
|
+
set :project_path, Proc.new{project_path}, true
|
107
|
+
end
|
108
|
+
|
109
|
+
def check_project
|
110
|
+
if settings.project_path.nil? || !File.exists?(File.expand_path(settings.project_path))
|
111
|
+
puts "Whoops! I need the path to a Git repo."
|
112
|
+
puts " $ git clone git@github.com:username/project.git project"
|
113
|
+
abort " $ ciquantum project"
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|