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 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
- Setup a project or two in that directory
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
- Setup continuous-integration for each project
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 three executable scripts
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 Twitter or Campfire? It's one line away.
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.3.3
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.3.3"
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{2009-11-30}
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 Twitter or Campfire? It's one line away.
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 :test_results
6
+ attr_accessor :runners
7
7
 
8
- def initialize(output_directory)
9
- @test_results = []
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(@output_directory) do
15
- @test_results.each do |result|
16
- update_project(result)
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
- update_projects
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(result)
35
- FileUtils.mkdir_p(result.project_name)
36
- Dir.chdir(result.project_name) do
37
- filename = filename_from_version(result.version_output)
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
- update_recent(result, filename)
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 = @test_results.map {|r| "#{r.project_name}"}
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 valid?
19
+ def check_for_valid_scripts
13
20
  check_script('update')
14
21
  check_script('version')
15
22
  check_script('test')
16
- return true
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
- Open3.popen3(script_path(name)) do |stdin, stdout, stderr|
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 invoke
55
- if valid?
56
- project_name = File.basename(@project_path)
57
- results = Result.new(project_name)
58
-
59
- results.version_output, results.version_error = run_script('version')
60
-
61
- if results.version_error.empty?
62
- results.update_output, results.update_error = run_script('update')
63
- else
64
- results.update_output, results.update_error = '', ''
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
- results.timestamp = Time.now
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