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.
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