crazy_ivan 0.3.3 → 1.0.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.
- data/README.rdoc +7 -3
- data/Rakefile +5 -1
- data/TODO +7 -1
- data/VERSION +1 -1
- data/bin/test_report2campfire +96 -0
- data/crazy_ivan.gemspec +19 -4
- data/lib/crazy_ivan/report_assembler.rb +74 -24
- data/lib/crazy_ivan/test_runner.rb +74 -28
- data/lib/crazy_ivan/vendor/open4-1.0.1/README +365 -0
- data/lib/crazy_ivan/vendor/open4-1.0.1/README.erb +365 -0
- data/lib/crazy_ivan/vendor/open4-1.0.1/Rakefile +225 -0
- data/lib/crazy_ivan/vendor/open4-1.0.1/lib/open4.rb +401 -0
- data/lib/crazy_ivan/vendor/open4-1.0.1/open4.gemspec +28 -0
- data/lib/crazy_ivan/vendor/open4-1.0.1/samples/bg.rb +21 -0
- data/lib/crazy_ivan/vendor/open4-1.0.1/samples/block.rb +19 -0
- data/lib/crazy_ivan/vendor/open4-1.0.1/samples/exception.rb +3 -0
- data/lib/crazy_ivan/vendor/open4-1.0.1/samples/simple.rb +15 -0
- data/lib/crazy_ivan/vendor/open4-1.0.1/samples/spawn.rb +16 -0
- data/lib/crazy_ivan/vendor/open4-1.0.1/samples/stdin_timeout.rb +9 -0
- data/lib/crazy_ivan/vendor/open4-1.0.1/samples/timeout.rb +37 -0
- data/lib/crazy_ivan/vendor/open4-1.0.1/white_box/leak.rb +17 -0
- data/lib/crazy_ivan/vendor/open4.rb +10 -0
- data/lib/crazy_ivan/version.rb +3 -3
- data/lib/crazy_ivan.rb +90 -42
- data/templates/css/ci.css +1 -0
- data/templates/index.html +40 -11
- data/test/crazy_ivan_test.rb +100 -18
- data/test/test_helper.rb +6 -0
- metadata +19 -3
data/README.rdoc
CHANGED
@@ -7,11 +7,11 @@ Crazy Ivan (CI) is simplest possible continuous integration tool.
|
|
7
7
|
Create a directory where your projects will live
|
8
8
|
$ mkdir /var/continuous-integration
|
9
9
|
|
10
|
-
|
10
|
+
Place some project(s) in that directory
|
11
11
|
$ cd /var/continuous-integration
|
12
12
|
$ git clone git://github.com/edward/active_merchant.git
|
13
13
|
|
14
|
-
|
14
|
+
Set up continuous integration for each project
|
15
15
|
$ crazy_ivan setup # creates example ci scripts in
|
16
16
|
# each project (see How this works)
|
17
17
|
|
@@ -48,7 +48,7 @@ Crazy Ivan (CI) is simplest possible continuous integration tool.
|
|
48
48
|
of these dirs /active_merchant
|
49
49
|
========> /active_shipping
|
50
50
|
|
51
|
-
=> within each directory, it expects
|
51
|
+
=> within each directory, it expects four executable scripts
|
52
52
|
to execute at the /:
|
53
53
|
|
54
54
|
/shopify
|
@@ -56,6 +56,7 @@ Crazy Ivan (CI) is simplest possible continuous integration tool.
|
|
56
56
|
update
|
57
57
|
version
|
58
58
|
test
|
59
|
+
conclusion
|
59
60
|
|
60
61
|
* crazy_ivan first executes `update` and captures the output:
|
61
62
|
|
@@ -85,6 +86,9 @@ Crazy Ivan (CI) is simplest possible continuous integration tool.
|
|
85
86
|
* At each of these three steps, the output is repackaged
|
86
87
|
into a .json file to be consumed in the directory holding
|
87
88
|
the static html.
|
89
|
+
|
90
|
+
* crazy_ivan then executes `conclusion`, passing it the same results packaged
|
91
|
+
in the .json file used in the static html view.
|
88
92
|
|
89
93
|
|
90
94
|
== Copyright and Credits
|
data/Rakefile
CHANGED
@@ -11,12 +11,16 @@ begin
|
|
11
11
|
By keeping test reports in json, per-project CI configuration in 3 probably-one-line scripts, things are kept simple, quick, and super extensible.
|
12
12
|
|
13
13
|
Want to use git, svn, or hg? No problem.
|
14
|
-
Need to fire off results to
|
14
|
+
Need to fire off results to Campfire? It's built-in.
|
15
15
|
|
16
16
|
CI depends on cron."
|
17
17
|
gem.email = "edward@edwardog.net"
|
18
18
|
gem.homepage = "http://github.com/edward/crazy_ivan"
|
19
19
|
gem.authors = ["Edward Ocampo-Gooding"]
|
20
|
+
gem.executables = ["crazy_ivan", "test_report2campfire"]
|
21
|
+
gem.default_executable = "crazy_ivan"
|
22
|
+
gem.files = FileList['.gitignore', '*.gemspec', 'lib/**/*', 'bin/*', 'templates/**/*', '[A-Z]*', 'test/**/*'].to_a
|
23
|
+
|
20
24
|
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
21
25
|
end
|
22
26
|
|
data/TODO
CHANGED
@@ -30,4 +30,10 @@
|
|
30
30
|
|
31
31
|
* Use datejs to parse the timestamp to easily produce deltas (i.e. "Last test ran 2 days ago")
|
32
32
|
|
33
|
-
* Do something like use a file-lock to prevent overlapping runs and notify in the index.html that it's happening
|
33
|
+
* Do something like use a file-lock to prevent overlapping runs and notify in the index.html that it's happening
|
34
|
+
|
35
|
+
* Write a man page with Ron: http://github.com/rtomayko/ron
|
36
|
+
|
37
|
+
* Refactor and document Syslog to have method names that actually match the syslog levels (like #error instead of #err )
|
38
|
+
|
39
|
+
* Don't overwrite existing config files and warn when skipping
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
1.0.0
|
@@ -0,0 +1,96 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
# This code nabbed from http://developer.37signals.com/campfire/ [http://developer.37signals.com/campfire/campfire.rb]
|
4
|
+
|
5
|
+
require 'rubygems'
|
6
|
+
require 'uri'
|
7
|
+
require 'httparty'
|
8
|
+
require 'json'
|
9
|
+
|
10
|
+
class Campfire
|
11
|
+
include HTTParty
|
12
|
+
|
13
|
+
base_uri 'https://37s.campfirenow.com'
|
14
|
+
basic_auth 'find_your_auth_key_on_member_slash_edit', 'x'
|
15
|
+
headers 'Content-Type' => 'application/json'
|
16
|
+
|
17
|
+
def self.rooms
|
18
|
+
Campfire.get('/rooms.json')["rooms"]
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.room(room_id)
|
22
|
+
Room.new(room_id)
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.user(id)
|
26
|
+
Campfire.get("/users/#{id}.json")["user"]
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
class Room
|
31
|
+
attr_reader :room_id
|
32
|
+
|
33
|
+
def initialize(room_id)
|
34
|
+
@room_id = room_id
|
35
|
+
end
|
36
|
+
|
37
|
+
def join
|
38
|
+
post 'join'
|
39
|
+
end
|
40
|
+
|
41
|
+
def leave
|
42
|
+
post 'leave'
|
43
|
+
end
|
44
|
+
|
45
|
+
def lock
|
46
|
+
post 'lock'
|
47
|
+
end
|
48
|
+
|
49
|
+
def unlock
|
50
|
+
post 'unlock'
|
51
|
+
end
|
52
|
+
|
53
|
+
def message(message)
|
54
|
+
send_message message
|
55
|
+
end
|
56
|
+
|
57
|
+
def paste(paste)
|
58
|
+
send_message paste, 'PasteMessage'
|
59
|
+
end
|
60
|
+
|
61
|
+
def play_sound(sound)
|
62
|
+
send_message sound, 'SoundMessage'
|
63
|
+
end
|
64
|
+
|
65
|
+
def transcript
|
66
|
+
get('transcript')['messages']
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
def send_message(message, type = 'Textmessage')
|
72
|
+
post 'speak', :body => {:message => {:body => message, :type => type}}.to_json
|
73
|
+
end
|
74
|
+
|
75
|
+
def get(action, options = {})
|
76
|
+
Campfire.get room_url_for(action), options
|
77
|
+
end
|
78
|
+
|
79
|
+
def post(action, options = {})
|
80
|
+
Campfire.post room_url_for(action), options
|
81
|
+
end
|
82
|
+
|
83
|
+
def room_url_for(action)
|
84
|
+
"/room/#{room_id}/#{action}.json"
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
report = JSON.parse(STDIN.read)
|
89
|
+
|
90
|
+
campfire_url = URI.parse(ARGV[0])
|
91
|
+
Campfire.base_uri campfire_url.scheme + '://' + campfire_url.host
|
92
|
+
Campfire.basic_auth ARGV[1], 'x'
|
93
|
+
|
94
|
+
campfire_room_id = campfire_url.path[/\d+/]
|
95
|
+
campfire_room = Campfire.room(campfire_room_id)
|
96
|
+
campfire_room.message "#{report['project_name']} broke. Please take a look at #{ARGV[2]}"
|
data/crazy_ivan.gemspec
CHANGED
@@ -5,22 +5,22 @@
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
7
|
s.name = %q{crazy_ivan}
|
8
|
-
s.version = "0.
|
8
|
+
s.version = "1.0.0"
|
9
9
|
|
10
10
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
11
|
s.authors = ["Edward Ocampo-Gooding"]
|
12
|
-
s.date = %q{
|
12
|
+
s.date = %q{2010-01-10}
|
13
13
|
s.default_executable = %q{crazy_ivan}
|
14
14
|
s.description = %q{Continuous integration should really just be a script that captures the output of running your project update & test commands and presents recent results in a static html page.
|
15
15
|
|
16
16
|
By keeping test reports in json, per-project CI configuration in 3 probably-one-line scripts, things are kept simple, quick, and super extensible.
|
17
17
|
|
18
18
|
Want to use git, svn, or hg? No problem.
|
19
|
-
Need to fire off results to
|
19
|
+
Need to fire off results to Campfire? It's built-in.
|
20
20
|
|
21
21
|
CI depends on cron.}
|
22
22
|
s.email = %q{edward@edwardog.net}
|
23
|
-
s.executables = ["crazy_ivan"]
|
23
|
+
s.executables = ["crazy_ivan", "test_report2campfire"]
|
24
24
|
s.extra_rdoc_files = [
|
25
25
|
"LICENSE",
|
26
26
|
"README.rdoc",
|
@@ -34,6 +34,7 @@ Gem::Specification.new do |s|
|
|
34
34
|
"TODO",
|
35
35
|
"VERSION",
|
36
36
|
"bin/crazy_ivan",
|
37
|
+
"bin/test_report2campfire",
|
37
38
|
"crazy_ivan.gemspec",
|
38
39
|
"lib/crazy_ivan.rb",
|
39
40
|
"lib/crazy_ivan/html_asset_crush.rb",
|
@@ -154,6 +155,20 @@ Gem::Specification.new do |s|
|
|
154
155
|
"lib/crazy_ivan/vendor/json-1.1.7/tools/fuzz.rb",
|
155
156
|
"lib/crazy_ivan/vendor/json-1.1.7/tools/server.rb",
|
156
157
|
"lib/crazy_ivan/vendor/json.rb",
|
158
|
+
"lib/crazy_ivan/vendor/open4-1.0.1/README",
|
159
|
+
"lib/crazy_ivan/vendor/open4-1.0.1/README.erb",
|
160
|
+
"lib/crazy_ivan/vendor/open4-1.0.1/Rakefile",
|
161
|
+
"lib/crazy_ivan/vendor/open4-1.0.1/lib/open4.rb",
|
162
|
+
"lib/crazy_ivan/vendor/open4-1.0.1/open4.gemspec",
|
163
|
+
"lib/crazy_ivan/vendor/open4-1.0.1/samples/bg.rb",
|
164
|
+
"lib/crazy_ivan/vendor/open4-1.0.1/samples/block.rb",
|
165
|
+
"lib/crazy_ivan/vendor/open4-1.0.1/samples/exception.rb",
|
166
|
+
"lib/crazy_ivan/vendor/open4-1.0.1/samples/simple.rb",
|
167
|
+
"lib/crazy_ivan/vendor/open4-1.0.1/samples/spawn.rb",
|
168
|
+
"lib/crazy_ivan/vendor/open4-1.0.1/samples/stdin_timeout.rb",
|
169
|
+
"lib/crazy_ivan/vendor/open4-1.0.1/samples/timeout.rb",
|
170
|
+
"lib/crazy_ivan/vendor/open4-1.0.1/white_box/leak.rb",
|
171
|
+
"lib/crazy_ivan/vendor/open4.rb",
|
157
172
|
"lib/crazy_ivan/version.rb",
|
158
173
|
"templates/css/ci.css",
|
159
174
|
"templates/index.html",
|
@@ -3,21 +3,46 @@ class ReportAssembler
|
|
3
3
|
ROOT_PATH = File.expand_path(File.dirname(__FILE__))
|
4
4
|
TEMPLATES_PATH = File.join(ROOT_PATH, *%w[.. .. templates])
|
5
5
|
|
6
|
-
attr_accessor :
|
6
|
+
attr_accessor :runners
|
7
7
|
|
8
|
-
def initialize(output_directory)
|
9
|
-
@
|
8
|
+
def initialize(projects_directory, output_directory)
|
9
|
+
@runners = []
|
10
|
+
@projects_directory = projects_directory
|
10
11
|
@output_directory = output_directory
|
11
12
|
end
|
12
13
|
|
13
14
|
def generate
|
14
|
-
Dir.chdir(@
|
15
|
-
|
16
|
-
|
15
|
+
Dir.chdir(@projects_directory) do
|
16
|
+
Dir['*'].each do |dir|
|
17
|
+
if File.directory?(dir)
|
18
|
+
runners << TestRunner.new(File.join(@projects_directory, dir))
|
19
|
+
end
|
17
20
|
end
|
18
|
-
|
19
|
-
|
21
|
+
end
|
22
|
+
|
23
|
+
Dir.chdir(@output_directory) do
|
20
24
|
update_index
|
25
|
+
update_projects
|
26
|
+
|
27
|
+
runners.each do |runner|
|
28
|
+
# REFACTOR to run this block in multiple threads to have multi-project testing
|
29
|
+
|
30
|
+
# Write the first version of the report with just the start time to currently_building.json
|
31
|
+
runner.start!
|
32
|
+
update_project(runner)
|
33
|
+
|
34
|
+
# Update the report in currently_building.json with the update output and error
|
35
|
+
runner.update!
|
36
|
+
update_project(runner)
|
37
|
+
|
38
|
+
# Update the report in currently_building.json with the version output and error
|
39
|
+
runner.version!
|
40
|
+
update_project(runner)
|
41
|
+
|
42
|
+
# Empty the currently_building.json and add to recents.json this new report with the test output and error
|
43
|
+
runner.test!
|
44
|
+
update_project(runner)
|
45
|
+
end
|
21
46
|
end
|
22
47
|
end
|
23
48
|
|
@@ -28,30 +53,55 @@ class ReportAssembler
|
|
28
53
|
s += "-#{Dir["#{s}*.json"].size}"
|
29
54
|
end
|
30
55
|
|
31
|
-
s
|
56
|
+
return s
|
57
|
+
end
|
58
|
+
|
59
|
+
def nullify_successful_exit_status_for_json_templates(results)
|
60
|
+
filtered_results = YAML.load(results.to_yaml)
|
61
|
+
|
62
|
+
filtered_results[:version][:exit_status] = nil if filtered_results[:version][:exit_status] == '0'
|
63
|
+
filtered_results[:update][:exit_status] = nil if filtered_results[:update][:exit_status] == '0'
|
64
|
+
filtered_results[:test][:exit_status] = nil if filtered_results[:test][:exit_status] == '0'
|
65
|
+
|
66
|
+
return filtered_results
|
67
|
+
end
|
68
|
+
|
69
|
+
def flush_build_progress
|
70
|
+
File.open("currently_building.json", 'w+') do |f|
|
71
|
+
f.puts({}.to_json)
|
72
|
+
end
|
32
73
|
end
|
33
74
|
|
34
|
-
def update_project(
|
35
|
-
FileUtils.mkdir_p(
|
36
|
-
Dir.chdir(
|
37
|
-
|
75
|
+
def update_project(runner)
|
76
|
+
FileUtils.mkdir_p(runner.project_name)
|
77
|
+
Dir.chdir(runner.project_name) do
|
78
|
+
|
79
|
+
filename = ''
|
80
|
+
|
81
|
+
if runner.still_building?
|
82
|
+
filename = 'currently_building'
|
83
|
+
else
|
84
|
+
if runner.results[:version][:exit_status] == '0'
|
85
|
+
filename = filename_from_version(runner.results[:version][:output])
|
86
|
+
else
|
87
|
+
filename = filename_from_version(runner.results[:version][:error])
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
38
91
|
File.open("#{filename}.json", 'w+') do |f|
|
39
|
-
f.puts(
|
40
|
-
"version" => [result.version_error, result.version_output].join,
|
41
|
-
"timestamp" => result.timestamp,
|
42
|
-
"update" => result.update_output,
|
43
|
-
"update_error" => result.update_error,
|
44
|
-
"test" => result.test_output,
|
45
|
-
"test_error" => result.test_error
|
46
|
-
}.to_json)
|
92
|
+
f.puts(nullify_successful_exit_status_for_json_templates(runner.results).to_json)
|
47
93
|
end
|
48
94
|
|
49
|
-
|
95
|
+
if runner.finished?
|
96
|
+
Syslog.debug "Runner is FINISHED"
|
97
|
+
flush_build_progress
|
98
|
+
update_recent(runner.results, filename)
|
99
|
+
end
|
50
100
|
end
|
51
101
|
end
|
52
102
|
|
53
103
|
def update_recent(result, filename)
|
54
|
-
recent_versions_json = File.open('recent.json', File::RDWR|File::CREAT).read
|
104
|
+
recent_versions_json = File.open('recent.json', File::RDWR | File::CREAT).read
|
55
105
|
|
56
106
|
recent_versions = []
|
57
107
|
|
@@ -68,7 +118,7 @@ class ReportAssembler
|
|
68
118
|
end
|
69
119
|
|
70
120
|
def update_projects
|
71
|
-
projects = @
|
121
|
+
projects = @runners.map {|r| r.project_name }
|
72
122
|
|
73
123
|
File.open('projects.json', 'w+') do |f|
|
74
124
|
f.print({"projects" => projects}.to_json)
|
@@ -1,19 +1,26 @@
|
|
1
1
|
require 'open3'
|
2
2
|
|
3
3
|
class TestRunner
|
4
|
-
|
5
|
-
class Result < Struct.new(:project_name, :version_output, :update_output, :test_output, :version_error, :update_error, :test_error, :timestamp)
|
6
|
-
end
|
7
|
-
|
8
4
|
def initialize(project_path)
|
9
5
|
@project_path = project_path
|
6
|
+
@results = {:project_name => File.basename(@project_path),
|
7
|
+
:version => {:output => '', :error => '', :exit_status => ''},
|
8
|
+
:update => {:output => '', :error => '', :exit_status => ''},
|
9
|
+
:test => {:output => '', :error => '', :exit_status => ''},
|
10
|
+
:timestamp => {:start => nil, :finish => nil}}
|
11
|
+
end
|
12
|
+
|
13
|
+
attr_reader :results
|
14
|
+
|
15
|
+
def project_name
|
16
|
+
@results[:project_name]
|
10
17
|
end
|
11
18
|
|
12
|
-
def
|
19
|
+
def check_for_valid_scripts
|
13
20
|
check_script('update')
|
14
21
|
check_script('version')
|
15
22
|
check_script('test')
|
16
|
-
|
23
|
+
check_script('conclusion')
|
17
24
|
end
|
18
25
|
|
19
26
|
def script_path(name)
|
@@ -39,40 +46,79 @@ class TestRunner
|
|
39
46
|
def run_script(name)
|
40
47
|
output = ''
|
41
48
|
error = ''
|
49
|
+
exit_status = ''
|
42
50
|
|
43
51
|
Dir.chdir(@project_path) do
|
44
|
-
|
52
|
+
status = Open4::popen4(script_path(name)) do |pid, stdin, stdout, stderr|
|
45
53
|
stdin.close # Close to prevent hanging if the script wants input
|
46
54
|
output = stdout.read
|
47
55
|
error = stderr.read
|
48
56
|
end
|
57
|
+
|
58
|
+
exit_status = status.exitstatus
|
49
59
|
end
|
50
60
|
|
51
|
-
return output.chomp, error.chomp
|
61
|
+
return output.chomp, error.chomp, exit_status.to_s
|
52
62
|
end
|
53
63
|
|
54
|
-
def
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
end
|
66
|
-
|
67
|
-
if results.update_error.empty?
|
68
|
-
results.test_output, results.test_error = run_script('test')
|
69
|
-
else
|
70
|
-
results.test_output, results.test_error = '', ''
|
64
|
+
def run_conclusion_script
|
65
|
+
|
66
|
+
# REFACTOR do this asynchronously so the next tests don't wait on running the conclusion
|
67
|
+
|
68
|
+
Dir.chdir(@project_path) do
|
69
|
+
Syslog.debug "Passing report to conclusion script at #{script_path('conclusion')}"
|
70
|
+
errors = ''
|
71
|
+
status = Open4.popen4(script_path('conclusion')) do |pid, stdin, stdout, stderr|
|
72
|
+
stdin.puts @results.to_json
|
73
|
+
stdin.close
|
74
|
+
errors = stderr.read
|
71
75
|
end
|
72
76
|
|
73
|
-
|
74
|
-
|
75
|
-
return results
|
77
|
+
Syslog.err(errors) if status.exitstatus != '0'
|
78
|
+
Syslog.debug "Finished executing conclusion script"
|
76
79
|
end
|
80
|
+
|
81
|
+
rescue Errno::EPIPE
|
82
|
+
Syslog.err "Unknown issue in writing to conclusion script."
|
83
|
+
end
|
84
|
+
|
85
|
+
def start!
|
86
|
+
# REFACTOR to just report whichever scripts are invalid
|
87
|
+
check_for_valid_scripts
|
88
|
+
|
89
|
+
@results[:timestamp][:start] = Time.now
|
90
|
+
Syslog.info "Starting CI for #{project_name}"
|
91
|
+
end
|
92
|
+
|
93
|
+
def update!
|
94
|
+
Syslog.debug "Updating #{project_name}"
|
95
|
+
@results[:update][:output], @results[:update][:error], @results[:update][:exit_status] = run_script('update')
|
96
|
+
end
|
97
|
+
|
98
|
+
def version!
|
99
|
+
if @results[:update][:exit_status] == '0'
|
100
|
+
Syslog.debug "Acquiring build version for #{project_name}"
|
101
|
+
@results[:version][:output], @results[:version][:error], @results[:version][:exit_status] = run_script('version')
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def test!
|
106
|
+
if @results[:version][:exit_status] == '0'
|
107
|
+
Syslog.debug "Testing #{@results[:project_name]} build #{@results[:version][:output]}"
|
108
|
+
@results[:test][:output], @results[:test][:error], @results[:test][:exit_status] = run_script('test')
|
109
|
+
else
|
110
|
+
Syslog.debug "Failed to test #{project_name}; version exit status was #{@results[:version][:exit_status]}"
|
111
|
+
end
|
112
|
+
|
113
|
+
@results[:timestamp][:finish] = Time.now
|
114
|
+
run_conclusion_script
|
115
|
+
end
|
116
|
+
|
117
|
+
def finished?
|
118
|
+
@results[:timestamp][:finish]
|
119
|
+
end
|
120
|
+
|
121
|
+
def still_building?
|
122
|
+
!finished?
|
77
123
|
end
|
78
124
|
end
|