juici 0.0.0.alpha1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (76) hide show
  1. data/.gitignore +2 -0
  2. data/Gemfile +5 -0
  3. data/Gemfile.lock +74 -0
  4. data/Procfile +1 -0
  5. data/README.md +138 -0
  6. data/Rakefile +33 -0
  7. data/TODO.md +28 -0
  8. data/bin/juici +7 -0
  9. data/bin/juicic +54 -0
  10. data/config/mongoid.yml.sample +25 -0
  11. data/juici-interface.gemspec +19 -0
  12. data/juici.gemspec +32 -0
  13. data/lib/juici.rb +24 -0
  14. data/lib/juici/app.rb +67 -0
  15. data/lib/juici/build_environment.rb +38 -0
  16. data/lib/juici/build_logic.rb +37 -0
  17. data/lib/juici/build_queue.rb +111 -0
  18. data/lib/juici/callback.rb +26 -0
  19. data/lib/juici/config.rb +11 -0
  20. data/lib/juici/controllers.rb +6 -0
  21. data/lib/juici/controllers/base.rb +26 -0
  22. data/lib/juici/controllers/build_queue.rb +14 -0
  23. data/lib/juici/controllers/builds.rb +74 -0
  24. data/lib/juici/controllers/index.rb +20 -0
  25. data/lib/juici/controllers/trigger.rb +85 -0
  26. data/lib/juici/database.rb +25 -0
  27. data/lib/juici/exceptions.rb +2 -0
  28. data/lib/juici/find_logic.rb +11 -0
  29. data/lib/juici/helpers/form_helpers.rb +11 -0
  30. data/lib/juici/helpers/html_helpers.rb +4 -0
  31. data/lib/juici/helpers/url_helpers.rb +38 -0
  32. data/lib/juici/interface.rb +13 -0
  33. data/lib/juici/models/build.rb +190 -0
  34. data/lib/juici/models/build_process.rb +7 -0
  35. data/lib/juici/models/project.rb +9 -0
  36. data/lib/juici/server.rb +172 -0
  37. data/lib/juici/version.rb +8 -0
  38. data/lib/juici/views/README.markdown +138 -0
  39. data/lib/juici/views/about.erb +16 -0
  40. data/lib/juici/views/builds.erb +7 -0
  41. data/lib/juici/views/builds/edit.erb +23 -0
  42. data/lib/juici/views/builds/list.erb +27 -0
  43. data/lib/juici/views/builds/new.erb +43 -0
  44. data/lib/juici/views/builds/show.erb +4 -0
  45. data/lib/juici/views/index.erb +30 -0
  46. data/lib/juici/views/layout.erb +44 -0
  47. data/lib/juici/views/not_found.erb +3 -0
  48. data/lib/juici/views/partials/builds/debug.erb +22 -0
  49. data/lib/juici/views/partials/builds/output.erb +1 -0
  50. data/lib/juici/views/partials/builds/show.erb +19 -0
  51. data/lib/juici/views/partials/builds/sidebar.erb +13 -0
  52. data/lib/juici/views/partials/index/recently_built.erb +19 -0
  53. data/lib/juici/views/queue/list.erb +6 -0
  54. data/lib/juici/views/redirect.erb +0 -0
  55. data/lib/juici/views/support.erb +6 -0
  56. data/lib/juici/watcher.rb +48 -0
  57. data/public/favicon.ico +0 -0
  58. data/public/images/black_denim.png +0 -0
  59. data/public/styles/builds.css +62 -0
  60. data/public/styles/juici.css +226 -0
  61. data/public/vendor/bootstrap.css +6004 -0
  62. data/public/vendor/bootstrap.js +2036 -0
  63. data/public/vendor/img/glyphicons-halflings-white.png +0 -0
  64. data/public/vendor/jquery.js +9440 -0
  65. data/sample/mongod.conf +5 -0
  66. data/script/cibuild +10 -0
  67. data/spec/build_callback_spec.rb +54 -0
  68. data/spec/build_environment_spec.rb +53 -0
  69. data/spec/build_process_spec.rb +96 -0
  70. data/spec/build_queue_spec.rb +63 -0
  71. data/spec/controllers/builds_spec.rb +68 -0
  72. data/spec/controllers/index_spec.rb +28 -0
  73. data/spec/juici_app_spec.rb +8 -0
  74. data/spec/models/build_spec.rb +54 -0
  75. data/spec/spec_helper.rb +26 -0
  76. 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,2 @@
1
+ class AbortBuild < Exception
2
+ end
@@ -0,0 +1,11 @@
1
+ module Juici
2
+ module FindLogic
3
+
4
+ def find_or_raise(exc, by)
5
+ self.where(by).first.tap do |rec|
6
+ raise exc if rec.nil?
7
+ end
8
+ end
9
+
10
+ end
11
+ 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,4 @@
1
+ require 'cgi'
2
+ def escaped(content)
3
+ CGI.escapeHTML content
4
+ 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,13 @@
1
+ module Juici
2
+ module BuildStatus
3
+ PASS = "success"
4
+ FAIL = "failed"
5
+ START = "started"
6
+ WAIT = "waiting"
7
+ end
8
+
9
+ module Routes
10
+ NEW_BUILD = '/builds/new'
11
+ end
12
+
13
+ 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
@@ -0,0 +1,7 @@
1
+ # The abstraction around a build.
2
+ #
3
+ # Workflow
4
+ #
5
+ # hit
6
+ # POST /trigger/:project
7
+ # -> environment
@@ -0,0 +1,9 @@
1
+ module Juici
2
+ class Project
3
+ include Mongoid::Document
4
+ extend FindLogic
5
+
6
+ field :name, type: String
7
+
8
+ end
9
+ end
@@ -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