octopusci 0.2.3 → 0.3.0

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 (39) hide show
  1. data/README.markdown +132 -0
  2. data/bin/octopusci-reset-redis +26 -0
  3. data/bin/octopusci-skel +2 -7
  4. data/bin/octopusci-tentacles +2 -2
  5. data/config.ru +1 -1
  6. data/lib/octopusci.rb +3 -7
  7. data/lib/octopusci/config.rb +63 -49
  8. data/lib/octopusci/errors.rb +2 -0
  9. data/lib/octopusci/helpers.rb +16 -15
  10. data/lib/octopusci/io.rb +70 -0
  11. data/lib/octopusci/job.rb +145 -34
  12. data/lib/octopusci/job_store.rb +67 -0
  13. data/lib/octopusci/notifier.rb +7 -17
  14. data/lib/octopusci/notifier/job_complete.html.erb +76 -3
  15. data/lib/octopusci/queue.rb +14 -10
  16. data/lib/octopusci/server.rb +17 -20
  17. data/lib/octopusci/server/views/index.erb +3 -4
  18. data/lib/octopusci/server/views/job.erb +3 -3
  19. data/lib/octopusci/server/views/job_summary.erb +18 -18
  20. data/lib/octopusci/server/views/layout.erb +6 -5
  21. data/lib/octopusci/stage_locker.rb +11 -7
  22. data/lib/octopusci/version.rb +1 -1
  23. data/lib/octopusci/worker_launcher.rb +1 -1
  24. data/spec/lib/octopusci/config_spec.rb +195 -0
  25. data/spec/lib/octopusci/io_spec.rb +64 -0
  26. data/spec/lib/octopusci/job_spec.rb +122 -0
  27. data/spec/lib/octopusci/job_store_spec.rb +155 -0
  28. data/spec/lib/octopusci/notifier_spec.rb +0 -15
  29. data/spec/lib/octopusci/queue_spec.rb +122 -0
  30. data/spec/lib/octopusci/server_spec.rb +92 -1
  31. data/spec/lib/octopusci/stage_locker_spec.rb +94 -0
  32. data/spec/spec_helper.rb +8 -0
  33. metadata +39 -58
  34. data/README +0 -63
  35. data/bin/octopusci-db-migrate +0 -10
  36. data/db/migrate/0001_init.rb +0 -29
  37. data/db/migrate/0002_add_status_job.rb +0 -19
  38. data/lib/octopusci/notifier/job_complete.text.erb +0 -5
  39. data/lib/octopusci/schema.rb +0 -140
@@ -1,8 +1,7 @@
1
1
  <script type="text/javascript" charset="utf-8">
2
2
  $(document).ready(function() {
3
3
  $('#jobs').delegate("a.show_hide_job_output", 'click', function() {
4
- $('.job_output').not(this).hide();
5
- var job_cont = $(this).closest('.job_summary').next('.job_content')
4
+ var job_cont = $(this).closest('.job_summary').next('.job_content');
6
5
  var job_out = job_cont.find('.job_output').first();
7
6
  job_out.toggle();
8
7
  return false;
@@ -20,12 +19,12 @@
20
19
  </script>
21
20
  <div id="jobs">
22
21
  <% @jobs.each do |j| %>
23
- <div id="job_<%= j.id%>" class="job status_<%= j.status %>" data-status-url="/jobs/<%= j.id %>/status" data-output-url="/jobs/<%= j.id %>/output" data-summary-url="/jobs/<%= j.id %>/ajax_summary">
22
+ <div id="job_<%= j['id']%>" class="job status_<%= j['status'] %>" data-status-url="/jobs/<%= j['id'] %>/status" data-output-url="/jobs/<%= j['id'] %>/output" data-summary-url="/jobs/<%= j['id'] %>/ajax_summary">
24
23
  <div class="job_summary">
25
24
  <%= erb :job_summary, :locals => { :j => j } %>
26
25
  </div>
27
26
  <div class="job_content">
28
- <div class="job_output"><pre><code><%= j.output %></code></pre></div>
27
+ <div class="job_output"><pre><code><%= Octopusci::IO.new(j).read_all_out %></code></pre></div>
29
28
  </div>
30
29
  </div>
31
30
  <% end %>
@@ -11,16 +11,16 @@
11
11
  }, 5000);
12
12
  });
13
13
  </script>
14
- <div id="job_<%= @job.id%>" class="job status_<%= @job.status %>" data-status-url="/jobs/<%= @job.id %>/status" data-output-url="/jobs/<%= @job.id %>/output" data-silent-output-url="/jobs/<%= @job.id %>/silent_output" data-summary-url="/jobs/<%= @job.id %>/ajax_summary">
14
+ <div id="job_<%= @job['id']%>" class="job status_<%= @job['status'] %>" data-status-url="/jobs/<%= @job['id'] %>/status" data-output-url="/jobs/<%= @job['id'] %>/output" data-silent-output-url="/jobs/<%= @job['id'] %>/silent_output" data-summary-url="/jobs/<%= @job['id'] %>/ajax_summary">
15
15
  <div class="job_summary">
16
16
  <%= erb :job_summary, :locals => { :j => @job } %>
17
17
  </div>
18
18
  <div class="job_content">
19
19
  Output:
20
- <div class="output job_output" style="display: block;"><pre><code><%= @job.output %></code></pre></div>
20
+ <div class="output job_output" style="display: block;"><pre><code><%= Octopusci::IO.new(@job).read_all_out %></code></pre></div>
21
21
  </div>
22
22
  <div class="job_content">
23
23
  Log:
24
- <div class="silent_output job_output" style="display: block;"><pre><code><%= @job.silent_output %></code></pre></div>
24
+ <div class="silent_output job_output" style="display: block;"><pre><code><%= Octopusci::IO.new(@job).read_all_log %></code></pre></div>
25
25
  </div>
26
26
  </div>
@@ -1,13 +1,13 @@
1
1
  <div class="job_header">
2
2
  <div class="job_title">
3
- <span class="status <%= j.status %>"></span>
3
+ <span class="status <%= j['status'] %>"></span>
4
4
  <div style="float: left;">
5
- <a href="<%= j.payload['repository']['url'] %>"><%= j.repo_name %></a> / <a href="/<%= j.repo_name %>/<%= j.branch_name %>/jobs"><%= j.branch_name %></a>
5
+ <a href="<%= j['payload']['repository']['url'] %>"><%= j['repo_name'] %></a> / <a href="/<%= j['repo_name'] %>/<%= j['branch_name'] %>/jobs"><%= j['branch_name'] %></a>
6
6
  </div>
7
7
  <div style="clear: both;"></div>
8
8
  </div>
9
9
  <div class="created_on">
10
- <%= j.created_at.strftime('%A, %B %d, %Y AT %I:%M%p') %>
10
+ <%= j['created_at'].strftime('%A, %B %d, %Y AT %I:%M%p') unless j['created_at'].nil? %>
11
11
  </div>
12
12
  <div style="clear: both;"></div>
13
13
  </div>
@@ -15,11 +15,11 @@
15
15
  <table>
16
16
  <tr>
17
17
  <td style="text-align: right;">Job:</td>
18
- <td style="width: 200px;"><a href="/jobs/<%= j.id %>"><%= j.id %></a></td>
18
+ <td style="width: 200px;"><a href="/jobs/<%= j['id'] %>"><%= j['id'] %></a></td>
19
19
  <td style="text-align: right;">Before Commit:</td>
20
20
  <td>
21
- <% if !j.payload['created'] %>
22
- <%= j.payload['before'] %>
21
+ <% if !j['payload']['created'] %>
22
+ <%= j['payload']['before'] %>
23
23
  <% else %>
24
24
  No before commit, remote branch was created with this push.
25
25
  <% end %>
@@ -28,49 +28,49 @@
28
28
  <tr>
29
29
  <td style="text-align: right;">Finished:</td>
30
30
  <td>
31
- <% if j.ended_at %>
32
- <%= j.ended_at.ago_in_words %>
31
+ <% if j['ended_at'] %>
32
+ <%= j['ended_at'].ago_in_words %>
33
33
  <% else %>
34
34
  -
35
35
  <% end %>
36
36
  </td>
37
37
  <td style="text-align: right;">After Commit:</td>
38
38
  <td>
39
- <%= j.payload['after'] %>
39
+ <%= j['payload']['after'] %>
40
40
  </td>
41
41
  </tr>
42
42
  <tr>
43
43
  <td style="text-align: right;">Duration:</td>
44
44
  <td>
45
- <% if j.ended_at && j.started_at %>
46
- <%= ((j.ended_at - j.started_at)/60).to_i %> minutes
45
+ <% if j['ended_at'] && j['started_at'] %>
46
+ <%= ((j['ended_at'] - j['started_at'])/60).to_i %> minutes
47
47
  <% else %>
48
48
  -
49
49
  <% end %>
50
50
  </td>
51
51
  <td style="text-align: right;">Compare:</td>
52
- <td><a href="<%= j.compare %>"><%= j.compare %></a></td>
52
+ <td><a href="<%= j['compare'] %>"><%= j['compare'] %></a></td>
53
53
  </tr>
54
54
  <tr>
55
55
  <td style="text-align: right;">Stage:</td>
56
- <td><%= j.stage %></td>
56
+ <td><%= j['stage'] %></td>
57
57
  <td style="text-align: right;">Pusher:</td>
58
58
  <td>
59
- <% if j.payload['pusher']['email'] %>
60
- <a href="mailto:<%= j.payload['pusher']['name'] %>"><%= j.payload['pusher']['name'] %></a>
59
+ <% if j['payload']['pusher']['email'] %>
60
+ <a href="mailto:<%= j['payload']['pusher']['name'] %>"><%= j['payload']['pusher']['name'] %></a>
61
61
  <% else %>
62
- <%= j.payload['pusher']['name'] %>
62
+ <%= j['payload']['pusher']['name'] %>
63
63
  <% end %>
64
64
  </td>
65
65
  </tr>
66
66
  <tr>
67
67
  <td style="text-align: right;">Output:</td>
68
68
  <td>
69
- <a class="show_hide_job_output" style="display: block; float: left;" href="#">toggle</a><div class="output_busy" style="float: left; margin-left: 5px; margin-top: 2px; <% if j.status == 'running' %>display: block;<% else %>display: none;<% end %>"><img src="/images/ajax-loader-1.gif" style="height: 12px;"/></div><div style="clear: both;"></div>
69
+ <a class="show_hide_job_output" style="display: block; float: left;" href="#">toggle</a><div class="output_busy" style="float: left; margin-left: 5px; margin-top: 2px; <% if j['status'] == 'running' %>display: block;<% else %>display: none;<% end %>"><img src="/images/ajax-loader-1.gif" style="height: 12px;"/></div><div style="clear: both;"></div>
70
70
  </td>
71
71
  <td style="text-align: right;">Log:</td>
72
72
  <td>
73
- <a target="_blank" href="/jobs/<%= j.id %>/silent_output">click here</a>
73
+ <a target="_blank" href="/jobs/<%= j['id'] %>/silent_output">click here</a>
74
74
  </td>
75
75
  </tr>
76
76
  </table>
@@ -103,25 +103,26 @@
103
103
  height: 26px;
104
104
  display: block;
105
105
  margin: 0 0 0 0;
106
+ background-position: 0px -74px; /* pending... in the case that no status specific class is provided but this is, the default case */
106
107
  }
107
108
 
108
- .successful {
109
+ .status.successful {
109
110
  background-position: 0px 0px;
110
111
  }
111
112
 
112
- .failed {
113
+ .status.failed {
113
114
  background-position: 0px -26px;
114
115
  }
115
116
 
116
- .running {
117
+ .status.running {
117
118
  background-position: 0px -52px;
118
119
  }
119
120
 
120
- .pending {
121
+ .status.pending {
121
122
  background-position: 0px -74px;
122
123
  }
123
124
 
124
- .error {
125
+ .status.error {
125
126
  background-position: 0px -26px;
126
127
  }
127
128
  </style>
@@ -8,28 +8,32 @@ module Octopusci
8
8
  end
9
9
 
10
10
  def self.clear
11
- Resque.redis.del('octopusci:stagelocker')
11
+ self.redis.del('octopusci:stagelocker')
12
12
  end
13
13
 
14
14
  def self.pop
15
- Resque.redis.lpop('octopusci:stagelocker')
15
+ self.redis.lpop('octopusci:stagelocker')
16
16
  end
17
17
 
18
18
  def self.rem(v)
19
- Resque.redis.lrem('octopusci:stagelocker', 1, v)
19
+ self.redis.lrem('octopusci:stagelocker', 1, v)
20
20
  end
21
21
 
22
22
  def self.push(v)
23
- Resque.redis.rpush('octopusci:stagelocker', v)
23
+ self.redis.rpush('octopusci:stagelocker', v)
24
24
  end
25
25
 
26
26
  def self.stages
27
- Octopusci::CONFIG['stages']
27
+ Octopusci::Config['stages']
28
28
  end
29
29
 
30
30
  def self.pool
31
- len = Resque.size('octopusci:stagelocker')
32
- Resque.peek('octopusci:stagelocker', 0, len)
31
+ len = self.redis.size('octopusci:stagelocker')
32
+ self.redis.peek('octopusci:stagelocker', 0, len)
33
+ end
34
+
35
+ def self.redis
36
+ Resque.redis
33
37
  end
34
38
  end
35
39
  end
@@ -1,3 +1,3 @@
1
1
  module Octopusci
2
- Version = VERSION = '0.2.3'
2
+ Version = VERSION = '0.3.0'
3
3
  end
@@ -27,7 +27,7 @@ module Octopusci
27
27
 
28
28
  worker_pids = []
29
29
 
30
- Octopusci::CONFIG['stages'].size.times do
30
+ Octopusci::Config['stages'].size.times do
31
31
  cur_pid = Process.fork do
32
32
  queues = ['octopusci:commit']
33
33
  worker = Resque::Worker.new(*queues)
@@ -0,0 +1,195 @@
1
+ require 'spec_helper'
2
+
3
+ describe Octopusci::ConfigStore do
4
+
5
+ before(:each) do
6
+ @config_store = Octopusci::ConfigStore.new
7
+ end
8
+
9
+ describe "#initialize" do
10
+ it "should create an instance of Octopusci::ConfigStore class with an empty options set" do
11
+ @config_store.options.should == {}
12
+ end
13
+ end
14
+
15
+ describe "#options" do
16
+ it "should return a hash of the options" do
17
+ @config_store.options.class.should == Hash
18
+ end
19
+ end
20
+
21
+ describe "#load" do
22
+ it "should load a yml file into the options set additively" do
23
+ tmp_file_path = "/tmp/octopusci_test_config.yml"
24
+ File.open(tmp_file_path, "w") do |f|
25
+ f << "jack:\n"
26
+ f << " jobs_path: \"/etc/octopusci/jobs\"\n"
27
+ f << " workspace_base_path: \"/Users/adeponte/.octopusci\"\n"
28
+ end
29
+
30
+ @config_store.load(tmp_file_path)
31
+
32
+ @config_store.options.should == { 'jack' => { 'jobs_path' => "/etc/octopusci/jobs", "workspace_base_path" => "/Users/adeponte/.octopusci" } }
33
+
34
+ tmp_file_path = "/tmp/octopusci_test_config.yml"
35
+ File.open(tmp_file_path, "w") do |f|
36
+ f << "joe:\n"
37
+ f << " hello: \"world\"\n"
38
+ end
39
+
40
+ @config_store.load(tmp_file_path)
41
+
42
+ @config_store.options.should == { 'jack' => { 'jobs_path' => "/etc/octopusci/jobs", "workspace_base_path" => "/Users/adeponte/.octopusci" }, 'joe' => { 'hello' => 'world' } }
43
+ end
44
+
45
+ it "should load a provided block into the options set additively" do
46
+ @config_store.load() do |c|
47
+ c['jackie'] = 'fool'
48
+ end
49
+
50
+ @config_store.options.should == { 'jackie' => 'fool' }
51
+
52
+ @config_store.load() do |c|
53
+ c['brown'] = 'ace'
54
+ end
55
+
56
+ @config_store.options.should == { 'jackie' => 'fool', 'brown' => 'ace' }
57
+ end
58
+
59
+ it "should load a yml file and a provided block into the options set additively" do
60
+ tmp_file_path = "/tmp/octopusci_test_config.yml"
61
+ File.open(tmp_file_path, "w") do |f|
62
+ f << "alice:\n"
63
+ f << " hi: \"there\"\n"
64
+ end
65
+
66
+ @config_store.load(tmp_file_path) do |c|
67
+ c['bob'] = 'hey alice'
68
+ end
69
+
70
+ @config_store.options.should == { 'alice' => { 'hi' => 'there' }, 'bob' => 'hey alice' }
71
+
72
+ tmp_file_path = "/tmp/octopusci_test_config.yml"
73
+ File.open(tmp_file_path, "w") do |f|
74
+ f << "cindy:\n"
75
+ f << " good: \"bye\"\n"
76
+ end
77
+
78
+ @config_store.load(tmp_file_path) do |c|
79
+ c['rachel'] = 'cya'
80
+ end
81
+
82
+ @config_store.options.should == { 'alice' => { 'hi' => 'there' }, 'bob' => 'hey alice', 'cindy' => { 'good' => 'bye' }, 'rachel' => 'cya' }
83
+ end
84
+ end
85
+
86
+ describe "#reset" do
87
+ it "should reset the options set back to an empty set" do
88
+ @config_store.load() do |c|
89
+ c['kitty'] = 'little'
90
+ end
91
+
92
+ @config_store.options.should == { 'kitty' => 'little' }
93
+
94
+ @config_store.reset()
95
+
96
+ @config_store.options.should == {}
97
+ end
98
+ end
99
+
100
+ describe "#reload" do
101
+ it "should reset the options set to an empty set and then perform a normal additive load" do
102
+ @config_store.load() do |c|
103
+ c['big'] = 'dog'
104
+ end
105
+
106
+ @config_store.options.should == { 'big' => 'dog' }
107
+
108
+ @config_store.reload() do |c|
109
+ c['albino'] = 'zebra'
110
+ end
111
+
112
+ @config_store.options.should == { 'albino' => 'zebra' }
113
+ end
114
+ end
115
+
116
+ describe "hash style getter" do
117
+ it "should return the value of the option with the given key" do
118
+ @config_store.load() do |c|
119
+ c['test_getter'] = 'winning'
120
+ end
121
+
122
+ @config_store['test_getter'].should == 'winning'
123
+ end
124
+
125
+ it "should raise Octopusci::ConfigStore::MissingConfigField if try to get a key that doesn't exist" do
126
+ lambda { @config_store['blowup'] }.should raise_error(Octopusci::ConfigStore::MissingConfigField)
127
+ end
128
+ end
129
+
130
+ describe "hash style setter" do
131
+ it "should set the value of the option with the given key to the given value" do
132
+ @config_store['test_setter'] = 'bi-winning'
133
+ @config_store.options.should == { 'test_setter' => 'bi-winning' }
134
+ end
135
+ end
136
+
137
+ describe "method style getter" do
138
+ it "should return the value of the option with the give key as the method name" do
139
+ @config_store.load() do |c|
140
+ c['pirate'] = 'hords'
141
+ end
142
+
143
+ @config_store.pirate.should == 'hords'
144
+ end
145
+
146
+ it "should raise Octopusci::ConfigStore::MissingConfigField if try to get a key that doesn't exist" do
147
+ lambda { @config_store.hippy }.should raise_error(Octopusci::ConfigStore::MissingConfigField)
148
+ end
149
+ end
150
+
151
+ describe "method style setter" do
152
+ it "should set the value of the option with the given key as the method name to the given value" do
153
+ @config_store.ship = 'it'
154
+
155
+ @config_store.options.should == { 'ship' => 'it' }
156
+ end
157
+ end
158
+
159
+ describe "#has_key?" do
160
+ it "should return a boolean value indicating whether or not there is an option in the option set with the given key" do
161
+ @config_store['foo'] = 1
162
+
163
+ @config_store.has_key?('foo').should be_true
164
+ @config_store.has_key?('bar').should be_false
165
+ end
166
+ end
167
+
168
+ describe "#after_load" do
169
+ it "should set the after load callback when given a block" do
170
+ @config_store.after_load do
171
+ @config_store['after_load_cb_test'] = 'wootwoot'
172
+ end
173
+
174
+ @config_store.options.should == {}
175
+
176
+ @config_store.load() do |c|
177
+ @config_store['feet'] = 'wet'
178
+ end
179
+
180
+ @config_store.options.should == { 'feet' => 'wet', 'after_load_cb_test' => 'wootwoot' }
181
+ end
182
+
183
+ it "should execute the after load callback when NOT given a block and the callback has been previously set" do
184
+ @config_store.after_load do
185
+ @config_store['after_load_cb_test'] = 'wootwoot'
186
+ end
187
+
188
+ @config_store.load() do |c|
189
+ @config_store['feet'] = 'wet'
190
+ end
191
+
192
+ @config_store.options.should == { 'feet' => 'wet', 'after_load_cb_test' => 'wootwoot' }
193
+ end
194
+ end
195
+ end
@@ -0,0 +1,64 @@
1
+ require 'spec_helper'
2
+
3
+ describe Octopusci::IO do
4
+ describe "#initialize" do
5
+ it "should construct an instance of Octopusci::IO" do
6
+ job = stub('job')
7
+ Octopusci::IO.new(job).should be_a(Octopusci::IO)
8
+ end
9
+ end
10
+
11
+ describe "#read_all_out" do
12
+ it "should open the output file if it exists" do
13
+ file = stub('file').as_null_object
14
+ File.stub(:exists?).and_return(true)
15
+ File.should_receive(:open).and_return(file)
16
+ Octopusci::IO.new({ 'id' => 23 }).read_all_out
17
+ end
18
+
19
+ it "should return all the file content if the output file exists" do
20
+ file = stub('file', :read => 'file content', :close => 0)
21
+ File.stub(:exists?).and_return(true)
22
+ File.stub(:open).and_return(file)
23
+ Octopusci::IO.new({ 'id' => 23 }).read_all_out.should == 'file content'
24
+ end
25
+
26
+ it "should return an empty string if the file does not exist" do
27
+ File.stub(:exists?).and_return(false)
28
+ Octopusci::IO.new({ 'id' => 23 }).read_all_out.should == ""
29
+ end
30
+ end
31
+
32
+ describe "#read_all_log" do
33
+ it "should open the log file if it exists" do
34
+ file = stub('file').as_null_object
35
+ File.stub(:exists?).and_return(true)
36
+ File.should_receive(:open).and_return(file)
37
+ Octopusci::IO.new({ 'id' => 23 }).read_all_log
38
+ end
39
+
40
+ it "should return all the file content if the log file exists" do
41
+ file = stub('file', :read => 'file content')
42
+ File.stub(:exists?).and_return(true)
43
+ File.stub(:open).and_return(file)
44
+ Octopusci::IO.new({ 'id' => 23 }).read_all_log.should == 'file content'
45
+ end
46
+
47
+ it "should return an empty string if the file does not exist" do
48
+ File.stub(:exists?).and_return(false)
49
+ Octopusci::IO.new({ 'id' => 23 }).read_all_log.should == ""
50
+ end
51
+ end
52
+
53
+ describe "#open_log_for_writing" do
54
+ end
55
+
56
+ describe "#open_out_for_writing" do
57
+ end
58
+
59
+ describe "#puts" do
60
+ end
61
+
62
+ describe "#log" do
63
+ end
64
+ end