crazy_ivan 0.3.3 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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