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
data/lib/juici/app.rb
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
at_exit do
|
2
|
+
Juici::App.shutdown
|
3
|
+
end
|
4
|
+
|
5
|
+
module Juici
|
6
|
+
class App
|
7
|
+
@@watchers = []
|
8
|
+
|
9
|
+
def self.shutdown
|
10
|
+
::Juici.dbgp "Shutting down Juici"
|
11
|
+
::Juici::Watcher.instance.shutdown!
|
12
|
+
|
13
|
+
shutdown_build_queue
|
14
|
+
end
|
15
|
+
|
16
|
+
attr_reader :opts
|
17
|
+
def initialize(opts={})
|
18
|
+
Database.initialize!
|
19
|
+
@opts = opts
|
20
|
+
reset_stale_children
|
21
|
+
start_watcher
|
22
|
+
init_build_queue
|
23
|
+
reload_unfinished_work
|
24
|
+
start_queue
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def init_build_queue
|
30
|
+
$build_queue ||= BuildQueue.new
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.shutdown_build_queue
|
34
|
+
if $build_queue
|
35
|
+
$build_queue.shutdown!
|
36
|
+
$build_queue = nil
|
37
|
+
end
|
38
|
+
|
39
|
+
# Find any remaining children and kill them
|
40
|
+
# Ensure that any killed builds will be retried
|
41
|
+
end
|
42
|
+
|
43
|
+
def start_watcher
|
44
|
+
::Juici::Watcher.instance.start
|
45
|
+
end
|
46
|
+
|
47
|
+
def reload_unfinished_work
|
48
|
+
# At this point no workers have started yet, we can safely assume that
|
49
|
+
# :started means aborted
|
50
|
+
Build.where(:status => Juici::BuildStatus::WAIT).each do |build|
|
51
|
+
$build_queue << build
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def reset_stale_children
|
56
|
+
Build.where(:status => Juici::BuildStatus::START).each do |build|
|
57
|
+
build[:status] = Juici::BuildStatus::WAIT
|
58
|
+
build.save!
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def start_queue
|
63
|
+
$build_queue.start!
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Juici
|
2
|
+
BUILD_SENSITIVE_VARIABLES = %w[RUBYOPT BUNDLE_GEMFILE RACK_ENV MONGOLAB_URI GEM_PATH WORKING_DIR]
|
3
|
+
class BuildEnvironment
|
4
|
+
|
5
|
+
attr_reader :env
|
6
|
+
def initialize
|
7
|
+
@env = ENV.to_hash.tap do |env|
|
8
|
+
BUILD_SENSITIVE_VARIABLES.each do |var|
|
9
|
+
env[var] = nil
|
10
|
+
end
|
11
|
+
env["BUNDLE_CONFIG"] = "/nonexistent"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def [](k)
|
16
|
+
env[k]
|
17
|
+
end
|
18
|
+
|
19
|
+
# XXX This is spectacular.
|
20
|
+
# Not in the good way
|
21
|
+
def load_json!(json)
|
22
|
+
return true if json == ""
|
23
|
+
loaded_json = JSON.load(json)
|
24
|
+
if loaded_json.is_a? Hash
|
25
|
+
env.merge!(loaded_json)
|
26
|
+
return true
|
27
|
+
end
|
28
|
+
false
|
29
|
+
rescue JSON::ParserError
|
30
|
+
return false
|
31
|
+
end
|
32
|
+
|
33
|
+
def to_hash
|
34
|
+
env
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'tempfile'
|
3
|
+
module Juici
|
4
|
+
module BuildLogic
|
5
|
+
|
6
|
+
def spawn_build
|
7
|
+
raise "No such work tree" unless FileUtils.mkdir_p(worktree)
|
8
|
+
spawn(command, worktree)
|
9
|
+
rescue AbortBuild
|
10
|
+
:buildaborted
|
11
|
+
end
|
12
|
+
|
13
|
+
def kill!
|
14
|
+
warn! "Killed!"
|
15
|
+
if pid = self[:pid]
|
16
|
+
Process.kill(15, pid)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def spawn(cmd, dir)
|
23
|
+
@buffer = Tempfile.new('juici-xxxx')
|
24
|
+
Process.spawn(environment, cmd,
|
25
|
+
:chdir => dir,
|
26
|
+
:in => "/dev/null",
|
27
|
+
:out => @buffer.fileno,
|
28
|
+
:err => [:child, :out]
|
29
|
+
)
|
30
|
+
rescue Errno::ENOENT
|
31
|
+
:enoent
|
32
|
+
rescue TypeError
|
33
|
+
:invalidcommand
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
# An object representing a queue. It merely manages creating child processes
|
2
|
+
# and their priority, reaping them is a job for BuildThread
|
3
|
+
#
|
4
|
+
module Juici
|
5
|
+
class BuildQueue
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@builds = []
|
9
|
+
@child_pids = []
|
10
|
+
# This is never expired, for now
|
11
|
+
@builds_by_pid = {}
|
12
|
+
@started = false
|
13
|
+
end
|
14
|
+
|
15
|
+
def shutdown!
|
16
|
+
@child_pids.each do |pid|
|
17
|
+
::Juici.dbgp "Killing off child pid #{pid}"
|
18
|
+
Process.kill(15, pid)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# Pushing a Build object into the BuildQueue is expressing that you want it
|
23
|
+
# run
|
24
|
+
def <<(other)
|
25
|
+
@builds << other
|
26
|
+
bump!
|
27
|
+
end
|
28
|
+
|
29
|
+
def current_min_priority
|
30
|
+
@builds.collect(&:priority).compact.min || 1
|
31
|
+
end
|
32
|
+
|
33
|
+
def next_child
|
34
|
+
@builds.sort_by(&:priority).first
|
35
|
+
end
|
36
|
+
|
37
|
+
def purge(by, build)
|
38
|
+
@builds.reject! do |i|
|
39
|
+
build.send(by) == i.send(by)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def delete(id)
|
44
|
+
purge(:_id, OpenStruct.new(:_id => id))
|
45
|
+
end
|
46
|
+
|
47
|
+
# Magic hook that starts a process if there's a good reason to.
|
48
|
+
# Stopgap measure that means you can knock on this if there's a chance we
|
49
|
+
# should start a process
|
50
|
+
def bump!
|
51
|
+
return unless @started
|
52
|
+
update_children
|
53
|
+
if not_working? && work_to_do?
|
54
|
+
Juici.dbgp "Starting another child process"
|
55
|
+
next_child.tap do |child|
|
56
|
+
if pid = child.build!
|
57
|
+
Juici.dbgp "Started child: #{pid}"
|
58
|
+
@child_pids << pid
|
59
|
+
@builds_by_pid[pid] = child
|
60
|
+
else
|
61
|
+
Juici.dbgp "Child #{child} failed to start"
|
62
|
+
bump! # Ruby's recursion isn't great, but re{try,do} may as well be
|
63
|
+
# undefined behaviour here.
|
64
|
+
end
|
65
|
+
end
|
66
|
+
else
|
67
|
+
Juici.dbgp "I have quite enough to do"
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def update_children
|
72
|
+
@child_pids.select! do |pid|
|
73
|
+
return false if pid.nil?
|
74
|
+
begin
|
75
|
+
Process.kill(0, pid)
|
76
|
+
true
|
77
|
+
rescue Errno::ESRCH
|
78
|
+
false
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def get_build_by_pid(pid)
|
84
|
+
@builds_by_pid[pid]
|
85
|
+
end
|
86
|
+
|
87
|
+
def not_working?
|
88
|
+
@child_pids.length == 0
|
89
|
+
end
|
90
|
+
|
91
|
+
def work_to_do?
|
92
|
+
@builds.length > 0
|
93
|
+
end
|
94
|
+
|
95
|
+
def currently_building
|
96
|
+
@child_pids.map do |pid|
|
97
|
+
get_build_by_pid(pid)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def start!
|
102
|
+
@started = true
|
103
|
+
bump!
|
104
|
+
end
|
105
|
+
|
106
|
+
def builds
|
107
|
+
@builds
|
108
|
+
end
|
109
|
+
|
110
|
+
end
|
111
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
module Juici
|
3
|
+
class Callback
|
4
|
+
|
5
|
+
attr_reader :url
|
6
|
+
attr_accessor :payload
|
7
|
+
|
8
|
+
def initialize(url, pl=nil)
|
9
|
+
@url = URI(url)
|
10
|
+
@payload = pl if pl
|
11
|
+
end
|
12
|
+
|
13
|
+
def process!
|
14
|
+
Net::HTTP.start(url.host, url.port) do |http|
|
15
|
+
request = Net::HTTP::Post.new(url.request_uri)
|
16
|
+
request.body = payload
|
17
|
+
|
18
|
+
http.request request # Net::HTTPResponse object
|
19
|
+
end
|
20
|
+
rescue SocketError => e
|
21
|
+
# We don't get a reference to build any more, can't warn :(
|
22
|
+
# TODO Throw a warning on the build
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
end
|
data/lib/juici/config.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
module Juici::Controllers
|
2
|
+
class Base
|
3
|
+
|
4
|
+
NotFound = Sinatra::NotFound
|
5
|
+
|
6
|
+
attr_accessor :params
|
7
|
+
def initialize(params)
|
8
|
+
@params = params
|
9
|
+
end
|
10
|
+
|
11
|
+
def build_opts(opts)
|
12
|
+
default_opts.merge(opts)
|
13
|
+
end
|
14
|
+
|
15
|
+
def default_opts
|
16
|
+
{
|
17
|
+
:styles => styles
|
18
|
+
}
|
19
|
+
end
|
20
|
+
|
21
|
+
def styles
|
22
|
+
[]
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Juici::Controllers
|
2
|
+
class BuildQueue < Base
|
3
|
+
|
4
|
+
def list
|
5
|
+
builds = $build_queue.builds.sort_by(&:priority)
|
6
|
+
yield [:"queue/list", opts(:builds => builds)]
|
7
|
+
end
|
8
|
+
|
9
|
+
def opts(_opts={})
|
10
|
+
{:active => :queue}.merge(_opts)
|
11
|
+
end
|
12
|
+
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
module Juici::Controllers
|
2
|
+
class Builds < Base
|
3
|
+
|
4
|
+
def list
|
5
|
+
params[:page] = params[:page] ? params[:page].to_i : 0
|
6
|
+
|
7
|
+
project = ::Juici::Project.find_or_raise(NotFound, name: params[:project])
|
8
|
+
|
9
|
+
builds = ::Juici::Build.where(parent: project.name)
|
10
|
+
|
11
|
+
pages = (builds.count.to_f / ::Juici::Config.builds_per_page).ceil
|
12
|
+
|
13
|
+
builds = builds.desc(:_id).
|
14
|
+
limit(::Juici::Config.builds_per_page).
|
15
|
+
skip(params[:page].to_i * ::Juici::Config.builds_per_page)
|
16
|
+
|
17
|
+
yield [:"builds/list", build_opts({:project => project, :builds => builds, :pages => pages})]
|
18
|
+
end
|
19
|
+
|
20
|
+
def show
|
21
|
+
project = ::Juici::Project.find_or_raise(NotFound, name: params[:project])
|
22
|
+
build = ::Juici::Build.find_or_raise(NotFound, parent: project.name, _id: params[:id])
|
23
|
+
# return 404 unless project && build
|
24
|
+
yield [:"builds/show", build_opts({:project => project, :build => build})]
|
25
|
+
end
|
26
|
+
|
27
|
+
def kill
|
28
|
+
project = ::Juici::Project.find_or_raise(NotFound, name: params[:project])
|
29
|
+
build = ::Juici::Build.find_or_raise(NotFound, parent: project.name, _id: params[:id])
|
30
|
+
|
31
|
+
::Juici.dbgp "Killing off build #{build[:_id]}"
|
32
|
+
build.kill! if build.status == ::Juici::BuildStatus::START
|
33
|
+
return build
|
34
|
+
end
|
35
|
+
|
36
|
+
def cancel
|
37
|
+
project = ::Juici::Project.find_or_raise(NotFound, name: params[:project])
|
38
|
+
build = ::Juici::Build.find_or_raise(NotFound, parent: project.name, _id: params[:id])
|
39
|
+
|
40
|
+
::Juici.dbgp "Cancelling build #{build[:_id]}"
|
41
|
+
build.cancel if build.status == ::Juici::BuildStatus::WAIT
|
42
|
+
return build
|
43
|
+
end
|
44
|
+
|
45
|
+
def new
|
46
|
+
yield [:"builds/new", {:active => :new_build}]
|
47
|
+
end
|
48
|
+
|
49
|
+
def styles
|
50
|
+
["builds"]
|
51
|
+
end
|
52
|
+
|
53
|
+
def edit
|
54
|
+
project = ::Juici::Project.find_or_raise(NotFound, name: params[:project])
|
55
|
+
build = ::Juici::Build.find_or_raise(NotFound, parent: project.name, _id: params[:id])
|
56
|
+
# return 404 unless project && build
|
57
|
+
yield [:"builds/edit", {:project => project, :build => build}]
|
58
|
+
end
|
59
|
+
|
60
|
+
def update!
|
61
|
+
project = ::Juici::Project.find_or_raise(NotFound, name: params[:project])
|
62
|
+
build = ::Juici::Build.find_or_raise(NotFound, parent: project.name, _id: params[:id])
|
63
|
+
|
64
|
+
::Juici::Build::EDITABLE_ATTRIBUTES[:string].each do |attr|
|
65
|
+
build[attr] = params[attr] if params[attr]
|
66
|
+
end
|
67
|
+
# binding.pry
|
68
|
+
build.tap do |b|
|
69
|
+
b.save!
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Juici::Controllers
|
2
|
+
class Index
|
3
|
+
|
4
|
+
def index
|
5
|
+
yield [:index, {:active => :index}]
|
6
|
+
end
|
7
|
+
|
8
|
+
def about
|
9
|
+
content = GitHub::Markdown.render(File.read("lib/juici/views/README.markdown"))
|
10
|
+
content.gsub!(/<h(\d+)>/, '<h\1 class="block-header">')
|
11
|
+
yield [:about, {:active => :about, :content => content}]
|
12
|
+
end
|
13
|
+
|
14
|
+
def support
|
15
|
+
yield [:support, {:active => :support}]
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
@@ -0,0 +1,85 @@
|
|
1
|
+
module Juici::Controllers
|
2
|
+
class Trigger
|
3
|
+
|
4
|
+
attr_reader :project, :params
|
5
|
+
def initialize(project, params)
|
6
|
+
@project = ::Juici::Project.find_or_create_by(name: project)
|
7
|
+
@params = params
|
8
|
+
end
|
9
|
+
|
10
|
+
# Find an existing build, duplicate the sane parts of it.
|
11
|
+
def rebuild!
|
12
|
+
unless project = ::Juici::Project.where(name: params[:project]).first
|
13
|
+
not_found
|
14
|
+
end
|
15
|
+
unless build = ::Juici::Build.where(parent: project.name, _id: params[:id]).first
|
16
|
+
not_found
|
17
|
+
end
|
18
|
+
|
19
|
+
::Juici::Build.new_from(build).tap do |new_build|
|
20
|
+
new_build.save!
|
21
|
+
$build_queue << new_build
|
22
|
+
$build_queue.bump!
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def build!
|
27
|
+
environment = ::Juici::BuildEnvironment.new
|
28
|
+
::Juici::Build.new(parent: project.name).tap do |build|
|
29
|
+
# The seperation of concerns around this madness is horrifying
|
30
|
+
unless environment.load_json!(params['environment'])
|
31
|
+
build.warn!("Failed to parse environment")
|
32
|
+
end
|
33
|
+
|
34
|
+
build[:command] = build_command
|
35
|
+
build[:priority] = build_priority
|
36
|
+
build[:environment] = environment.to_hash
|
37
|
+
build[:callbacks] = callbacks
|
38
|
+
build[:title] = title if title_given?
|
39
|
+
|
40
|
+
build.save!
|
41
|
+
$build_queue << build
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# These accessors are not really the concern of a "Controller". On the
|
46
|
+
# other hand, this thing is also not a controller realistically, so I'm
|
47
|
+
# just going to carry on using this as a mechanism to poke at my model.
|
48
|
+
#
|
49
|
+
# I later plan to implement a more strictly bounded interface which bails
|
50
|
+
# instead of coercing.
|
51
|
+
def build_command
|
52
|
+
# TODO Throw 422 not acceptable if missing
|
53
|
+
params['command'].tap do |c|
|
54
|
+
raise "No command given" if c.nil?
|
55
|
+
# TODO Detect that we've recieved this from a browser session and only
|
56
|
+
# do this replacement there, we can trust API supplied data.
|
57
|
+
c.gsub!(/\r\n/, "\n")
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def build_priority
|
62
|
+
if params['priority'] =~ /^[0-9]+$/
|
63
|
+
params['priority'].to_i
|
64
|
+
else
|
65
|
+
1
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def callbacks
|
70
|
+
JSON.load(params['callbacks'])
|
71
|
+
rescue
|
72
|
+
[]
|
73
|
+
end
|
74
|
+
|
75
|
+
def title
|
76
|
+
params['title']
|
77
|
+
end
|
78
|
+
|
79
|
+
def title_given?
|
80
|
+
t = params['title']
|
81
|
+
!(t.nil? || t.empty?)
|
82
|
+
end
|
83
|
+
|
84
|
+
end
|
85
|
+
end
|