juici 0.0.0.alpha1
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 +2 -0
- data/Gemfile +5 -0
- data/Gemfile.lock +74 -0
- data/Procfile +1 -0
- data/README.md +138 -0
- data/Rakefile +33 -0
- data/TODO.md +28 -0
- data/bin/juici +7 -0
- data/bin/juicic +54 -0
- data/config/mongoid.yml.sample +25 -0
- data/juici-interface.gemspec +19 -0
- data/juici.gemspec +32 -0
- data/lib/juici.rb +24 -0
- data/lib/juici/app.rb +67 -0
- data/lib/juici/build_environment.rb +38 -0
- data/lib/juici/build_logic.rb +37 -0
- data/lib/juici/build_queue.rb +111 -0
- data/lib/juici/callback.rb +26 -0
- data/lib/juici/config.rb +11 -0
- data/lib/juici/controllers.rb +6 -0
- data/lib/juici/controllers/base.rb +26 -0
- data/lib/juici/controllers/build_queue.rb +14 -0
- data/lib/juici/controllers/builds.rb +74 -0
- data/lib/juici/controllers/index.rb +20 -0
- data/lib/juici/controllers/trigger.rb +85 -0
- data/lib/juici/database.rb +25 -0
- data/lib/juici/exceptions.rb +2 -0
- data/lib/juici/find_logic.rb +11 -0
- data/lib/juici/helpers/form_helpers.rb +11 -0
- data/lib/juici/helpers/html_helpers.rb +4 -0
- data/lib/juici/helpers/url_helpers.rb +38 -0
- data/lib/juici/interface.rb +13 -0
- data/lib/juici/models/build.rb +190 -0
- data/lib/juici/models/build_process.rb +7 -0
- data/lib/juici/models/project.rb +9 -0
- data/lib/juici/server.rb +172 -0
- data/lib/juici/version.rb +8 -0
- data/lib/juici/views/README.markdown +138 -0
- data/lib/juici/views/about.erb +16 -0
- data/lib/juici/views/builds.erb +7 -0
- data/lib/juici/views/builds/edit.erb +23 -0
- data/lib/juici/views/builds/list.erb +27 -0
- data/lib/juici/views/builds/new.erb +43 -0
- data/lib/juici/views/builds/show.erb +4 -0
- data/lib/juici/views/index.erb +30 -0
- data/lib/juici/views/layout.erb +44 -0
- data/lib/juici/views/not_found.erb +3 -0
- data/lib/juici/views/partials/builds/debug.erb +22 -0
- data/lib/juici/views/partials/builds/output.erb +1 -0
- data/lib/juici/views/partials/builds/show.erb +19 -0
- data/lib/juici/views/partials/builds/sidebar.erb +13 -0
- data/lib/juici/views/partials/index/recently_built.erb +19 -0
- data/lib/juici/views/queue/list.erb +6 -0
- data/lib/juici/views/redirect.erb +0 -0
- data/lib/juici/views/support.erb +6 -0
- data/lib/juici/watcher.rb +48 -0
- data/public/favicon.ico +0 -0
- data/public/images/black_denim.png +0 -0
- data/public/styles/builds.css +62 -0
- data/public/styles/juici.css +226 -0
- data/public/vendor/bootstrap.css +6004 -0
- data/public/vendor/bootstrap.js +2036 -0
- data/public/vendor/img/glyphicons-halflings-white.png +0 -0
- data/public/vendor/jquery.js +9440 -0
- data/sample/mongod.conf +5 -0
- data/script/cibuild +10 -0
- data/spec/build_callback_spec.rb +54 -0
- data/spec/build_environment_spec.rb +53 -0
- data/spec/build_process_spec.rb +96 -0
- data/spec/build_queue_spec.rb +63 -0
- data/spec/controllers/builds_spec.rb +68 -0
- data/spec/controllers/index_spec.rb +28 -0
- data/spec/juici_app_spec.rb +8 -0
- data/spec/models/build_spec.rb +54 -0
- data/spec/spec_helper.rb +26 -0
- metadata +290 -0
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'mongoid'
|
2
|
+
module Juici
|
3
|
+
class Database
|
4
|
+
class << self
|
5
|
+
def mongoid_config_file
|
6
|
+
%w[mongoid.yml mongoid.yml.sample].each do |file|
|
7
|
+
path = File.join("config", file)
|
8
|
+
return path if File.exist?(path)
|
9
|
+
end
|
10
|
+
raise "No database config file"
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialize!
|
14
|
+
if ::Juici.respond_to? :dbgp
|
15
|
+
::Juici.dbgp "initializing Mongoid with environment: #{ENV['RACK_ENV']}"
|
16
|
+
end
|
17
|
+
if ENV['RACK_ENV'] == "development"
|
18
|
+
Mongoid.logger.level = Logger::INFO
|
19
|
+
end
|
20
|
+
|
21
|
+
Mongoid.load!(mongoid_config_file)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
def form_at(route, fields, opts={})
|
2
|
+
form = ""
|
3
|
+
form << %Q{<form action="#{route}" method="post">\n}
|
4
|
+
|
5
|
+
fields.each do |name, value|
|
6
|
+
form << %Q{<input type="hidden" name="#{name}" value="#{value}">\n}
|
7
|
+
end
|
8
|
+
|
9
|
+
form << %Q{<button type="submit" class="btn">#{opts[:submit] || "submit"}</button>}
|
10
|
+
form << %Q{</form>}
|
11
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
def build_url_for(entity)
|
2
|
+
URI.escape case entity
|
3
|
+
when String
|
4
|
+
"/builds/#{entity}/list"
|
5
|
+
when ::Juici::Project
|
6
|
+
"/builds/#{entity.name}/list"
|
7
|
+
when ::Juici::Build
|
8
|
+
"/builds/#{entity[:parent]}/show/#{entity[:_id]}"
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def rebuild_url_for(entity)
|
13
|
+
URI.escape case entity
|
14
|
+
when ::Juici::Build
|
15
|
+
"/builds/#{entity[:parent]}/rebuild/#{entity[:_id]}"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def kill_url_for(entity)
|
20
|
+
URI.escape case entity
|
21
|
+
when ::Juici::Build
|
22
|
+
"/builds/kill"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def cancel_url_for(entity)
|
27
|
+
URI.escape case entity
|
28
|
+
when ::Juici::Build
|
29
|
+
"/builds/cancel"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def edit_url_for(entity)
|
34
|
+
URI.escape case entity
|
35
|
+
when ::Juici::Build
|
36
|
+
"/builds/#{entity[:parent]}/edit/#{entity[:_id]}"
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,190 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
module Juici
|
4
|
+
class Build
|
5
|
+
# A wrapper around the build process
|
6
|
+
|
7
|
+
include Mongoid::Document
|
8
|
+
include BuildLogic
|
9
|
+
include BuildStatus
|
10
|
+
extend FindLogic
|
11
|
+
# TODO Builds should probably be children of projects in the URL?
|
12
|
+
|
13
|
+
# Finder classmethods
|
14
|
+
def self.get_recent(n, opts={})
|
15
|
+
self.where(opts).
|
16
|
+
limit(n).
|
17
|
+
desc(:_id)
|
18
|
+
end
|
19
|
+
|
20
|
+
CLONABLE_FIELDS = [:command, :priority, :environment, :callbacks, :title, :parent]
|
21
|
+
EDITABLE_ATTRIBUTES = {
|
22
|
+
:string => [:priority, :title],
|
23
|
+
:array => [:environment, :callbacks]
|
24
|
+
}
|
25
|
+
|
26
|
+
def self.new_from(other)
|
27
|
+
new.tap do |b|
|
28
|
+
CLONABLE_FIELDS.each do |prop|
|
29
|
+
b[prop] = other[prop]
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
field :parent, type: String
|
35
|
+
field :command, type: String
|
36
|
+
field :environment, type: Hash
|
37
|
+
field :create_time, type: Time, :default => Proc.new { Time.now }
|
38
|
+
field :start_time, type: Time, :default => nil
|
39
|
+
field :end_time, type: Time, :default => nil
|
40
|
+
field :status, type: Symbol, :default => WAIT.to_sym
|
41
|
+
field :priority, type: Fixnum, :default => 1
|
42
|
+
field :pid, type: Fixnum
|
43
|
+
field :buffer, type: String
|
44
|
+
field :warnings, type: Array, :default => []
|
45
|
+
field :callbacks, type: Array, :default => []
|
46
|
+
field :title, type: String, :default => Proc.new { Time.now.to_s }
|
47
|
+
|
48
|
+
def set_status(value)
|
49
|
+
self.status= value
|
50
|
+
save!
|
51
|
+
end
|
52
|
+
|
53
|
+
def start!
|
54
|
+
self[:start_time] = Time.now
|
55
|
+
set_status START
|
56
|
+
end
|
57
|
+
|
58
|
+
def success!
|
59
|
+
finish
|
60
|
+
set_status PASS
|
61
|
+
process_callbacks
|
62
|
+
end
|
63
|
+
|
64
|
+
def failure!
|
65
|
+
finish
|
66
|
+
set_status FAIL
|
67
|
+
process_callbacks
|
68
|
+
end
|
69
|
+
|
70
|
+
def finish
|
71
|
+
self[:end_time] = Time.now
|
72
|
+
self[:output] = get_output
|
73
|
+
$build_queue.purge(:pid, self) if $build_queue
|
74
|
+
end
|
75
|
+
|
76
|
+
def cancel
|
77
|
+
warn! "Cancelled"
|
78
|
+
set_status FAIL
|
79
|
+
$build_queue.delete(self[:_id]) if $build_queue
|
80
|
+
end
|
81
|
+
|
82
|
+
def build!
|
83
|
+
start!
|
84
|
+
case pid = spawn_build
|
85
|
+
when Fixnum
|
86
|
+
Juici.dbgp "#{pid} away!"
|
87
|
+
self[:pid] = pid
|
88
|
+
self[:buffer] = @buffer.path
|
89
|
+
save!
|
90
|
+
return pid
|
91
|
+
when :enoent
|
92
|
+
warn! "No such command"
|
93
|
+
failure!
|
94
|
+
when :invalidcommand
|
95
|
+
warn! "Invalid command"
|
96
|
+
failure!
|
97
|
+
end
|
98
|
+
nil
|
99
|
+
end
|
100
|
+
|
101
|
+
def worktree
|
102
|
+
File.join(Config.workspace, parent)
|
103
|
+
rescue TypeError => e
|
104
|
+
warn! "Invalid workdir"
|
105
|
+
failure!
|
106
|
+
raise AbortBuild
|
107
|
+
end
|
108
|
+
|
109
|
+
# View helpers
|
110
|
+
def heading_color
|
111
|
+
case status
|
112
|
+
when WAIT
|
113
|
+
"build-heading-pending"
|
114
|
+
when FAIL
|
115
|
+
"build-heading-failed"
|
116
|
+
when PASS
|
117
|
+
"build-heading-success"
|
118
|
+
when START
|
119
|
+
"build-heading-started"
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def get_output
|
124
|
+
return "" unless self[:buffer]
|
125
|
+
File.open(self[:buffer], 'r') do |f|
|
126
|
+
f.rewind
|
127
|
+
f.read
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def display_title
|
132
|
+
# Catch old builds which didn't have a title
|
133
|
+
self[:title] || self[:create_time]
|
134
|
+
end
|
135
|
+
|
136
|
+
def link_title
|
137
|
+
"#{self[:parent]}/#{display_title}"
|
138
|
+
end
|
139
|
+
|
140
|
+
def warn!(msg)
|
141
|
+
warnings << msg
|
142
|
+
save!
|
143
|
+
end
|
144
|
+
|
145
|
+
def process_callbacks
|
146
|
+
callbacks.each do |callback_url|
|
147
|
+
c = Callback.new(callback_url)
|
148
|
+
c.payload = self.to_callback_json
|
149
|
+
c.process!
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
|
154
|
+
def to_callback_json
|
155
|
+
{
|
156
|
+
"project" => self[:parent],
|
157
|
+
"status" => self[:status],
|
158
|
+
"url" => build_url_for(self),
|
159
|
+
"time" => time_elapsed
|
160
|
+
}.to_json
|
161
|
+
end
|
162
|
+
|
163
|
+
def callbacks
|
164
|
+
self[:callbacks] || []
|
165
|
+
end
|
166
|
+
|
167
|
+
def time_elapsed
|
168
|
+
if self[:end_time]
|
169
|
+
self[:end_time] - self[:start_time]
|
170
|
+
elsif self[:start_time]
|
171
|
+
Time.now - self[:start_time]
|
172
|
+
else
|
173
|
+
nil
|
174
|
+
end
|
175
|
+
rescue
|
176
|
+
-1 # Throw an obviously impossible build time.
|
177
|
+
# This will only occur as a result of old builds.
|
178
|
+
end
|
179
|
+
|
180
|
+
# Use symbols internally
|
181
|
+
def status
|
182
|
+
self[:status].to_s
|
183
|
+
end
|
184
|
+
|
185
|
+
def status=(s)
|
186
|
+
self[:status] = s.to_sym
|
187
|
+
end
|
188
|
+
|
189
|
+
end
|
190
|
+
end
|
data/lib/juici/server.rb
ADDED
@@ -0,0 +1,172 @@
|
|
1
|
+
require 'sinatra/base'
|
2
|
+
require 'net/http' # for URI#escape
|
3
|
+
|
4
|
+
module Juici
|
5
|
+
class Server < Sinatra::Base
|
6
|
+
|
7
|
+
@@juici = nil
|
8
|
+
|
9
|
+
def juici
|
10
|
+
@@juici
|
11
|
+
end
|
12
|
+
|
13
|
+
helpers do
|
14
|
+
include Ansible
|
15
|
+
|
16
|
+
Dir[File.dirname(__FILE__) + "/helpers/**/*.rb"].each do |file|
|
17
|
+
load file
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
dir = File.dirname(File.expand_path(__FILE__))
|
22
|
+
|
23
|
+
def self.start(host, port)
|
24
|
+
@@juici = App.new
|
25
|
+
Juici::Server.run!(:host => host, :port => port) do |server|
|
26
|
+
[:INT, :TERM].each do |sig|
|
27
|
+
trap(sig) do
|
28
|
+
$stderr.puts "Shutting down JuiCI"
|
29
|
+
App.shutdown
|
30
|
+
server.respond_to?(:stop!) ? server.stop! : server.stop
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.rack_start(project_path)
|
37
|
+
self.new
|
38
|
+
end
|
39
|
+
|
40
|
+
set :views, "#{dir}/views"
|
41
|
+
set :public_folder, "public"
|
42
|
+
set :static, true
|
43
|
+
|
44
|
+
get '/' do
|
45
|
+
Controllers::Index.new.index do |template, opts|
|
46
|
+
erb(template, {}, opts)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
get '/about' do
|
51
|
+
Controllers::Index.new.about do |template, opts|
|
52
|
+
erb(template, {}, opts)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
get '/builds' do
|
57
|
+
Controllers::Index.new.builds do |template, opts|
|
58
|
+
erb(template, {}, opts)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
get '/support' do
|
63
|
+
Controllers::Index.new.support do |template, opts|
|
64
|
+
erb(template, {}, opts)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
post '/builds/new' do
|
69
|
+
build = Controllers::Trigger.new(params[:project], params).build!
|
70
|
+
@redirect_to = build_url_for(build)
|
71
|
+
erb(:redirect, {}, {})
|
72
|
+
end
|
73
|
+
|
74
|
+
post '/builds/:project/rebuild/:id' do
|
75
|
+
build = Controllers::Trigger.new(params[:project], params).rebuild!
|
76
|
+
@redirect_to = build_url_for(build)
|
77
|
+
erb(:redirect, {}, {})
|
78
|
+
end
|
79
|
+
|
80
|
+
post '/builds/:user/:project/rebuild/:id' do
|
81
|
+
params[:project] = "#{params[:user]}/#{params[:project]}"
|
82
|
+
build = Controllers::Trigger.new(params[:project], params).rebuild!
|
83
|
+
@redirect_to = build_url_for(build)
|
84
|
+
erb(:redirect, {}, {})
|
85
|
+
end
|
86
|
+
|
87
|
+
post '/builds/kill' do
|
88
|
+
build = Controllers::Builds.new(params).kill
|
89
|
+
@redirect_to = build_url_for(build)
|
90
|
+
erb(:redirect, {}, {})
|
91
|
+
end
|
92
|
+
|
93
|
+
post '/builds/cancel' do
|
94
|
+
build = Controllers::Builds.new(params).cancel
|
95
|
+
@redirect_to = build_url_for(build)
|
96
|
+
erb(:redirect, {}, {})
|
97
|
+
end
|
98
|
+
|
99
|
+
get ::Juici::Routes::NEW_BUILD do
|
100
|
+
Controllers::Builds.new(params).new do |template, opts|
|
101
|
+
erb(template, {}, opts)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
get '/builds/:project/list' do
|
106
|
+
Controllers::Builds.new(params).list do |template, opts|
|
107
|
+
erb(template, {}, opts)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
get '/builds/:user/:project/list' do
|
112
|
+
params[:project] = "#{params[:user]}/#{params[:project]}"
|
113
|
+
Controllers::Builds.new(params).list do |template, opts|
|
114
|
+
erb(template, {}, opts)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
get '/builds/:project/edit/:id' do
|
119
|
+
Controllers::Builds.new(params).edit do |template, opts|
|
120
|
+
erb(template, {}, opts)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
get '/builds/:user/:project/edit/:id' do
|
125
|
+
params[:project] = "#{params[:user]}/#{params[:project]}"
|
126
|
+
Controllers::Builds.new(params).edit do |template, opts|
|
127
|
+
erb(template, {}, opts)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
post '/builds/:project/edit/:id' do
|
132
|
+
build = Controllers::Builds.new(params).update!
|
133
|
+
@redirect_to = build_url_for(build)
|
134
|
+
erb(:redirect, {}, {})
|
135
|
+
end
|
136
|
+
|
137
|
+
post '/builds/:user/:project/edit/:id' do
|
138
|
+
params[:project] = "#{params[:user]}/#{params[:project]}"
|
139
|
+
build = Controllers::Builds.new(params).update!
|
140
|
+
@redirect_to = build_url_for(build)
|
141
|
+
erb(:redirect, {}, {})
|
142
|
+
end
|
143
|
+
|
144
|
+
get '/builds/:project/show/:id' do
|
145
|
+
Controllers::Builds.new(params).show do |template, opts|
|
146
|
+
erb(template, {}, opts)
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
get '/builds/:user/:project/show/:id' do
|
151
|
+
params[:project] = "#{params[:user]}/#{params[:project]}"
|
152
|
+
Controllers::Builds.new(params).show do |template, opts|
|
153
|
+
erb(template, {}, opts)
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
post '/trigger/:project' do
|
158
|
+
Controllers::Trigger.new(params[:project], params).build!
|
159
|
+
end
|
160
|
+
|
161
|
+
get '/queue' do
|
162
|
+
Controllers::BuildQueue.new(params).list do |template, opts|
|
163
|
+
erb(template, {}, opts)
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
not_found do
|
168
|
+
erb(:not_found, {}, {})
|
169
|
+
end
|
170
|
+
|
171
|
+
end
|
172
|
+
end
|