cruiseface 1.0.1

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/Manifest ADDED
@@ -0,0 +1,12 @@
1
+ Manifest
2
+ README.rdoc
3
+ TODO
4
+ bin/cruiseface
5
+ lib/cruise_face.rb
6
+ lib/cruise_face/console.rb
7
+ lib/cruise_face/pipeline.rb
8
+ lib/cruise_face/resource.rb
9
+ lib/cruise_face/stage.rb
10
+ lib/cruiseface.rb
11
+ rakefile
12
+ Rakefile
data/README.rdoc ADDED
@@ -0,0 +1,72 @@
1
+
2
+ = CruiseFace -- A ruby terminal Cruise dashboard
3
+
4
+ CruiseFace shows you a developer oriented Cruise dashboard in terminal, making
5
+ things simpler for monitoring Cruise pipelines.
6
+
7
+ == How it works
8
+
9
+ First time, you may try this, after installed the gem, type in 'cruiseface'
10
+
11
+ Then it'll ask you cruise site url, for example:
12
+
13
+ https://cruise.domain.com/cruise
14
+
15
+ Then your login user name and password
16
+
17
+ Then it will fetch all pipelines on the server, and list them, you need type in
18
+ the index number, e.g. 5,10
19
+
20
+ Done.
21
+
22
+ For myself, I open a terminal window using black background and change the font to 18pt.
23
+
24
+ Then specifying some environment variables in my ~/.profile:
25
+
26
+ export CRUISE_SERVER_URL='https://cruise.domain.com/cruise'
27
+ export CRUISE_LOGIN_NAME="xli"
28
+ export CRUISE_PIPELINE_NAMES='xx_trunk--CentOS5,xx_trunk--Windows2003'
29
+
30
+ Then type 'cruiseface' in the terminal, cruiseface will ask for the password.
31
+
32
+ Done.
33
+
34
+ == Why a new dashboard
35
+
36
+ When working with 2 pipelines, each has 4+ stages and has a stage with 50+ jobs,
37
+ the following information is enough and need for my daily work of checking build:
38
+
39
+ * Build pass/failed status
40
+
41
+ * How many jobs failed right now, even all jobs are building
42
+
43
+ * Who has responsibility for failed jobs
44
+
45
+ * Is failed job currently building? (When I committed a fix for the failed job, I really want to know whether the failed is fixed by my commit once the job is finished)
46
+
47
+ * some time, I may want to know the failing job name, but most of time, I just need the number of how many jobs failing.
48
+
49
+ * what's stage building, and who committed from last built stage (Cruise does not work in this way, so cruiseface also does not implement it yet, just display pipeline committers now)
50
+
51
+ * how many jobs are building, and I don't care about build success jobs
52
+
53
+ == Limitation
54
+
55
+ It only works on Mac, it could be run on other platform, but the color stuff won't work.
56
+
57
+ = Other stuff
58
+
59
+ Author: Li Xiao <iam@li-xiao.com>
60
+
61
+ Requires: Ruby 1.8.6 or later
62
+
63
+ License: Copyright 2010 by Li Xiao.
64
+ Released under an MIT-LICENSE. See the MIT-LICENSE.txt file
65
+ included in the distribution.
66
+
67
+ == Warranty
68
+
69
+ This software is provided "as is" and without any express or
70
+ implied warranties, including, without limitation, the implied
71
+ warranties of merchantibility and fitness for a particular
72
+ purpose.
data/Rakefile ADDED
@@ -0,0 +1,60 @@
1
+
2
+ $LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__) + '/lib')
3
+ require 'cruise_face'
4
+
5
+ require 'logger'
6
+
7
+ ActiveResource::Base.logger = Logger.new(STDOUT)
8
+ ActiveResource::Base.logger.level = Logger::INFO
9
+
10
+ namespace :dump do
11
+ task :pipeline_status do
12
+ CruiseFace.site('https://cruise01.thoughtworks.com/cruise').login('xli', Base64.decode64(ENV['LDAP_CODE']))
13
+ ps = CruiseFace::Resource.find_pipeline_status
14
+ File.open "./tast/data/ps.xml", 'w' do |f|
15
+ f.write ps.to_xml
16
+ end
17
+ end
18
+
19
+ task :pipeline_history do
20
+ CruiseFace.site('https://cruise01.thoughtworks.com/cruise').login('xli', Base64.decode64(ENV['LDAP_CODE']))
21
+ pipeline = CruiseFace.get(ENV['PN'])
22
+ File.open "./tast/data/#{pipeline.name}.xml", 'w' do |f|
23
+ f.write pipeline.resource.to_xml
24
+ end
25
+ pipeline.history.each do |history_pipeline|
26
+ history_pipeline.stages.each do |stage_name, stage|
27
+ begin
28
+ stage_status = history_pipeline.find_stage_status(stage)
29
+ File.open "./tast/data/#{history_pipeline.name}-#{history_pipeline.label}-#{stage.name}-#{stage.counter}.xml", 'w' do |f|
30
+ f.write stage_status.to_xml
31
+ end
32
+ rescue => e
33
+ #ignore cruise stage not exist problem when the pipeline is building
34
+ end
35
+ end
36
+ end
37
+ puts 'done'
38
+ end
39
+ task :stage_status do
40
+ CruiseFace.site('https://cruise01.thoughtworks.com/cruise').login('xli', Base64.decode64(ENV['LDAP_CODE']))
41
+ pipeline = CruiseFace::Resource.find_stage_status(ENV['PN'])
42
+ File.open "./tast/data/#{pipeline.name}.xml", 'w' do |f|
43
+ f.write pipeline.resource.to_xml
44
+ end
45
+ puts 'done'
46
+ end
47
+ end
48
+
49
+ require 'echoe'
50
+
51
+ Echoe.new('cruiseface', '1.0.1') do |p|
52
+ p.description = "CruiseFace arms to give a friendly Cruise Pipeline dashboard info for developers."
53
+ p.url = "https://github.com/xli/cruiseface"
54
+ p.author = "Li Xiao"
55
+ p.email = "iam@li-xiao.com"
56
+ p.ignore_pattern = "*.gemspec"
57
+ p.dependencies = ["activeresource", "highline"]
58
+ p.ignore_pattern = ['tast/*.rb', 'tast/**/*']
59
+ p.rdoc_options = %w(--main README.rdoc --inline-source --line-numbers --charset UTF-8)
60
+ end
data/TODO ADDED
@@ -0,0 +1,17 @@
1
+
2
+
3
+ new
4
+
5
+ # built a detail view instead of only show failing jobs name instead of count
6
+ # guess how much time left for a stage
7
+ # need a fancy UI
8
+ # when some jobs failed on one stage, and rerun the stage, the failure info does not showup
9
+ # committers of a stage should include all committers did commit after last time the stage built
10
+
11
+ done
12
+
13
+ # get all pipeline names for configuring monitor
14
+ # committers
15
+ # pipeline history parse
16
+ ## stage history
17
+ ## job history
data/bin/cruiseface ADDED
@@ -0,0 +1,104 @@
1
+ #!/usr/bin/env ruby
2
+ begin
3
+ require 'cruiseface'
4
+ rescue LoadError
5
+ require 'rubygems'
6
+ require 'cruiseface'
7
+ end
8
+
9
+ begin
10
+ require "highline/import"
11
+ rescue LoadError
12
+ require 'rubygems'
13
+ require "highline/import"
14
+ end
15
+
16
+ require 'optparse'
17
+
18
+ opts = OptionParser.new do |opts|
19
+ opts.banner = "CruiseFace usage: #{$0} [options]"
20
+ opts.separator ""
21
+ opts.separator "Environment properties CruiseFace uses as input if exists:"
22
+ opts.separator " CRUISE_SERVER_URL, CRUISE_LOGIN_NAME, CRUISE_PIPELINE_NAMES, CRUISE_LOGIN_PASSWORD"
23
+ opts.separator "Synopsis:"
24
+ opts.separator "cruiseface"
25
+ opts.separator "cruiseface -s 'http://domain.com/cruise' -u login_name -p pipeline1,pipeline2"
26
+ opts.separator ""
27
+ opts.separator "Options:"
28
+
29
+ opts.on("-s", "--server CRUISE_SERVER_URL", "Cruise server URL, including server context path") do |site|
30
+ ENV['CRUISE_SERVER_URL'] = site
31
+ end
32
+
33
+ opts.on("-u", "--user LOGIN_NAME", "Login name for login cruise server") do |login_name|
34
+ ENV['CRUISE_LOGIN_NAME'] = login_name
35
+ end
36
+
37
+ opts.on("-p pipeline1,pipeline2", Array, "Pipeline names to be monitored") do |names|
38
+ ENV['CRUISE_PIPELINE_NAMES'] = names.join(',')
39
+ end
40
+
41
+ opts.on("-w", "--password PASSWORD", "Password for login cruise server") do |password|
42
+ ENV['CRUISE_LOGIN_PASSWORD'] = password
43
+ end
44
+
45
+ opts.on_tail("-o", "--output", "Output cruise pipeline status and then exit") do
46
+ $cruise_fetch_once = true
47
+ end
48
+
49
+ opts.on_tail("-d", "--debug", "Open debug mode") do
50
+ ENV['CRUISE_FACE_DEBUG'] = 'true'
51
+ end
52
+
53
+ opts.on_tail("-v", "--version", "Show version") do
54
+ puts "cruiseface, version " + CruiseFace::VERSION
55
+ exit
56
+ end
57
+
58
+ opts.on_tail("-h", "--help", "Show this help doc") do
59
+ puts opts
60
+ exit
61
+ end
62
+ end
63
+
64
+ opts.parse!
65
+
66
+ if ENV['CRUISE_FACE_DEBUG']
67
+ ActiveResource::Base.logger = Logger.new(STDOUT)
68
+ ActiveResource::Base.logger.level = Logger::DEBUG
69
+ end
70
+
71
+ ENV['CRUISE_SERVER_URL'] ||= ask("Cruise server url (e.g. http://domain.com/cruise): ")
72
+ ENV['CRUISE_LOGIN_NAME'] ||= ask("Cruise server login name: ")
73
+ ENV['CRUISE_LOGIN_PASSWORD'] ||= ask("Cruise server login password: ") { |q| q.echo = false }
74
+
75
+ CruiseFace.site(ENV['CRUISE_SERVER_URL'])
76
+ CruiseFace.login(ENV['CRUISE_LOGIN_NAME'], ENV['CRUISE_LOGIN_PASSWORD'])
77
+
78
+ unless ENV['CRUISE_PIPELINE_NAMES']
79
+ puts "Fetching pipeline names from cruise server..."
80
+ puts "You can specify an environment variable CRUISE_PIPELINE_NAMES with names of pipelines split by comma to avoid fetching pipeline names every time you start cruiseface"
81
+ all_pipelines = CruiseFace.pipelines
82
+ all_pipelines.each_with_index do |p, index|
83
+ puts " #{index}. #{p}"
84
+ end
85
+ puts "Pipelines (type in indexes split by comma): "
86
+ pipelines = gets.strip.split(",").collect(&:strip)
87
+ if pipelines.all? {|p| p =~ /^\d+$/}
88
+ ENV['CRUISE_PIPELINE_NAMES'] = pipelines.collect {|p_index| all_pipelines[p_index.to_i]}.join(',')
89
+ else
90
+ ENV['CRUISE_PIPELINE_NAMES'] = pipelines.join(',')
91
+ end
92
+ end
93
+
94
+ begin
95
+ cruise_pipeline_names = ENV['CRUISE_PIPELINE_NAMES'].split(",").collect(&:strip)
96
+ if $cruise_fetch_once
97
+ cruise_pipeline_names.each do |name|
98
+ CruiseFace.output(name)
99
+ end
100
+ else
101
+ CruiseFace.console(cruise_pipeline_names)
102
+ end
103
+ rescue Interrupt
104
+ end
@@ -0,0 +1,38 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = %q{cruiseface}
5
+ s.version = "1.0.1"
6
+
7
+ s.required_rubygems_version = Gem::Requirement.new(">= 1.2") if s.respond_to? :required_rubygems_version=
8
+ s.authors = ["Li Xiao"]
9
+ s.date = %q{2010-03-12}
10
+ s.default_executable = %q{cruiseface}
11
+ s.description = %q{CruiseFace arms to give a friendly Cruise Pipeline dashboard info for developers.}
12
+ s.email = %q{iam@li-xiao.com}
13
+ s.executables = ["cruiseface"]
14
+ s.extra_rdoc_files = ["README.rdoc", "TODO", "bin/cruiseface", "lib/cruise_face.rb", "lib/cruise_face/console.rb", "lib/cruise_face/pipeline.rb", "lib/cruise_face/resource.rb", "lib/cruise_face/stage.rb", "lib/cruiseface.rb"]
15
+ s.files = ["Manifest", "README.rdoc", "TODO", "bin/cruiseface", "lib/cruise_face.rb", "lib/cruise_face/console.rb", "lib/cruise_face/pipeline.rb", "lib/cruise_face/resource.rb", "lib/cruise_face/stage.rb", "lib/cruiseface.rb", "rakefile", "Rakefile", "cruiseface.gemspec"]
16
+ s.homepage = %q{https://github.com/xli/cruiseface}
17
+ s.rdoc_options = ["--main", "README.rdoc", "--inline-source", "--line-numbers", "--charset", "UTF-8"]
18
+ s.require_paths = ["lib"]
19
+ s.rubyforge_project = %q{cruiseface}
20
+ s.rubygems_version = %q{1.3.5}
21
+ s.summary = %q{CruiseFace arms to give a friendly Cruise Pipeline dashboard info for developers.}
22
+
23
+ if s.respond_to? :specification_version then
24
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
25
+ s.specification_version = 3
26
+
27
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
28
+ s.add_runtime_dependency(%q<activeresource>, [">= 0"])
29
+ s.add_runtime_dependency(%q<highline>, [">= 0"])
30
+ else
31
+ s.add_dependency(%q<activeresource>, [">= 0"])
32
+ s.add_dependency(%q<highline>, [">= 0"])
33
+ end
34
+ else
35
+ s.add_dependency(%q<activeresource>, [">= 0"])
36
+ s.add_dependency(%q<highline>, [">= 0"])
37
+ end
38
+ end
@@ -0,0 +1,44 @@
1
+
2
+ require 'cruise_face/resource'
3
+ require 'cruise_face/pipeline'
4
+ require 'cruise_face/console'
5
+
6
+ module CruiseFace
7
+ VERSION = '1.0.1'
8
+ extend self
9
+
10
+ def site(site)
11
+ Resource.site = site
12
+ Resource.format = :json
13
+ # ignore the collection_name, we'll use action for find cruise resources,
14
+ # for cruise REST API does not fit with ActiveResource
15
+ Resource.collection_name = ''
16
+ self
17
+ end
18
+
19
+ def login(username, password)
20
+ Resource.user = username
21
+ Resource.password = password
22
+ self
23
+ end
24
+
25
+ def pipelines
26
+ Resource.find_pipeline_status.pipelines.collect(&:name)
27
+ end
28
+
29
+ def get(pipeline_name)
30
+ Model::Pipeline.new Resource.find_pipeline_history(pipeline_name)
31
+ end
32
+
33
+ def output(pipeline)
34
+ puts Console::UIBuilder.instance.fetch_pipeline_status(pipeline)
35
+ end
36
+
37
+ def output_text(pipeline)
38
+ puts Console::UIBuilder.instance.to_string(Console::UIBuilder.instance.fetch_pipeline_status(pipeline))
39
+ end
40
+
41
+ def console(pipeline_names)
42
+ Console.new(pipeline_names).start
43
+ end
44
+ end
@@ -0,0 +1,187 @@
1
+ require 'singleton'
2
+ require 'cgi'
3
+
4
+ module CruiseFace
5
+ class Console
6
+ class UIBuilder
7
+ include Singleton
8
+
9
+ def fetch_pipeline_status(pipeline)
10
+ xm = Builder::XmlMarkup.new
11
+ xm.instruct!
12
+ xm.pipeline(:name => pipeline) do
13
+ xm.stages(:type => 'array') do
14
+ CruiseFace.get(pipeline).stages.each do |stage|
15
+ stage.to_xml(xm)
16
+ end
17
+ end
18
+ end
19
+ rescue => e
20
+ xm = Builder::XmlMarkup.new
21
+ xm.instruct!
22
+ xm.errors(:pipeline_name => pipeline) { xm.error(e.message) }
23
+ end
24
+
25
+ def to_string(pipeline_status_xml)
26
+ if errors = Hash.from_xml(pipeline_status_xml)['errors']
27
+ return "Could not fetch pipeline #{errors['pipeline_name']} status, got error: #{errors['error']}\n"
28
+ end
29
+ pipeline = Resource.new(ActiveResource::Formats[:xml].decode(pipeline_status_xml))
30
+
31
+ status = "#{pipeline.name}:\n"
32
+ pipeline.stages.each do |stage|
33
+ status << " "
34
+ if stage.building_jobs.blank?
35
+ stage_info = "#{truncate(stage.name)}: [#{stage.latest.committers.collect(&:name).join(', ')}]"
36
+ if stage.latest.failed?
37
+ status << ' ' << failing_builds(stage_info)
38
+ else
39
+ status << ' ' << green_builds(stage_info)
40
+ end
41
+ else
42
+ status << spinner << build_running("#{truncate(stage.name)}: #{job_info(stage.building_jobs)} [#{stage.latest.committers.collect(&:name).join(', ')}]")
43
+ end
44
+
45
+ unless stage.failing_jobs.blank?
46
+ committers = stage.failing_jobs.collect(&:committers).flatten.collect(&:name).uniq.join(", ")
47
+ status << ', '
48
+ if stage.building_jobs.empty?
49
+ status << failing_builds("#{job_info(stage.failing_jobs)} [#{committers}]")
50
+ else
51
+ building_failed_jobs = stage.building_jobs.collect(&:name) & stage.failing_jobs.collect(&:name)
52
+ status << failing_builds(job_info(stage.failing_jobs))
53
+ status << build_running("(#{job_info(building_failed_jobs)})")
54
+ status << failing_builds(" [#{committers}]")
55
+ end
56
+ end
57
+ status << "\n"
58
+ end
59
+ status
60
+ rescue => e
61
+ status = "Unexpected error: #{e.message}" << "\n"
62
+ status << e.backtrace.join("\n") << "\n"
63
+ status << "\n"
64
+ status << "pipeline status xml:\n"
65
+ status << pipeline_status_xml << "\n"
66
+ status
67
+ end
68
+
69
+ def job_info(jobs)
70
+ return name_of(jobs.first) if jobs.size == 1
71
+ return "#{jobs.size} jobs" unless Console.show_details
72
+ return 0 if jobs.empty?
73
+ jobs.collect{|j| name_of(j)}.join(", ")
74
+ end
75
+
76
+ def name_of(job)
77
+ job.respond_to?(:name) ? job.name : job
78
+ end
79
+
80
+ def truncate(string, width=15)
81
+ if string.length <= width
82
+ string
83
+ else
84
+ string[0, width-3] + "..."
85
+ end
86
+ end
87
+
88
+ def spinner
89
+ colorize('5;33m', '*')
90
+ end
91
+ def build_running(text)
92
+ colorize('0;33m', text)
93
+ end
94
+ def failing_builds(text)
95
+ colorize('1;31m', text)
96
+ end
97
+ def green_builds(text)
98
+ colorize('0;32m', text)
99
+ end
100
+ def colorize(color, text)
101
+ "\e[#{color}#{text}\e[0m"
102
+ end
103
+
104
+ end
105
+
106
+ def self.show_details=(enable)
107
+ ENV['SHOW_CRUISE_DETAILS'] = enable.to_s
108
+ end
109
+
110
+ def self.show_details
111
+ ENV['SHOW_CRUISE_DETAILS'] == 'true'
112
+ end
113
+
114
+ def initialize(pipeline_names)
115
+ @pipeline_names = pipeline_names
116
+ @mutex = Mutex.new
117
+ @status = {}
118
+ end
119
+
120
+ def start
121
+ @pipeline_names.each do |name|
122
+ start_fetch(name)
123
+ end
124
+
125
+ puts "Initializing first report"
126
+ loop do
127
+ print '.'
128
+ STDOUT.flush
129
+ sleep 1
130
+ break unless @mutex.synchronize { @status.empty? }
131
+ end
132
+ puts "\nGot first report"
133
+ start_commander
134
+
135
+ loop do
136
+ refresh
137
+ sleep 5
138
+ end
139
+ end
140
+
141
+ def start_fetch(pipeline)
142
+ Thread.start do
143
+ loop do
144
+ pipeline_status = %x[#{$0} -o -p #{pipeline.inspect}]
145
+ @mutex.synchronize { @status[pipeline] = pipeline_status }
146
+ sleep 10
147
+ end
148
+ end
149
+ end
150
+
151
+ def refresh
152
+ status_dump = @mutex.synchronize { @status.dup }.collect do |pipeline, status|
153
+ UIBuilder.instance.to_string(status)
154
+ end.join("\n")
155
+ print %x{clear}
156
+ dashboard = "---- #{Time.now.strftime("%b %d %H:%M:%S")} ----\n"
157
+ dashboard << CGI.unescapeHTML(status_dump) << "\n"
158
+ dashboard << "Type 'open' to open Cruise dashboard" << "\n"
159
+ dashboard << "Type 'show/hide' to show/hide details" << "\n"
160
+ puts dashboard
161
+ end
162
+
163
+ def start_commander
164
+ Thread.start do
165
+ loop do
166
+ command = gets.strip
167
+ case command
168
+ when /^(open|start)$/
169
+ if Resource.site.nil?
170
+ puts "ERROR: cruise server url not found"
171
+ else
172
+ system "#{command} #{Resource.site.to_s.inspect}"
173
+ end
174
+ when /^hide$/
175
+ Console.show_details = false;
176
+ refresh
177
+ when /^show$/
178
+ Console.show_details = true;
179
+ refresh
180
+ else
181
+ puts "Unknown command"
182
+ end
183
+ end
184
+ end
185
+ end
186
+ end
187
+ end
@@ -0,0 +1,165 @@
1
+ require 'cruise_face/stage'
2
+
3
+ module CruiseFace::Model
4
+ class Pipeline
5
+ PASSED_STATUS = 'Passed'
6
+ FAILED_STATUS = 'Failed'
7
+ CANCELED_STATUS = 'Cancelled'
8
+ COMPLETED_STATUS = [PASSED_STATUS, FAILED_STATUS, CANCELED_STATUS]
9
+ BUILDING_STATUS = 'Building'
10
+ UNKNOWN_STATUS = 'Unknown'
11
+
12
+ class History
13
+ class Pipeline
14
+ attr_reader :resource, :name, :label
15
+ def initialize(name, resource)
16
+ @name = name
17
+ @resource = resource
18
+ @label = @resource.label
19
+ end
20
+
21
+ def stages
22
+ @stages || init_stages
23
+ end
24
+
25
+ def committers
26
+ @resource.materialRevisions.first.modifications.collect(&:user).uniq
27
+ end
28
+
29
+ def find_jobs(stage)
30
+ find_stage_status(stage).stage.builds.collect { |build| Job.new stage, build }
31
+ end
32
+
33
+ def find_stage_status(stage)
34
+ params = {:pipelineName => @name, :label => @label, :stageName => stage.name, :counter => stage.counter}
35
+ CruiseFace::Resource.find_stage_status(params)
36
+ end
37
+
38
+ def to_s
39
+ "pipeline[#{@name}<#{@label}>]"
40
+ end
41
+
42
+ private
43
+ def init_stages
44
+ @stages = {}
45
+ @resource.stages.each do |stage_resource|
46
+ stage = Stage.new(self, stage_resource)
47
+ @stages[stage.name] = stage
48
+ end
49
+ @stages
50
+ end
51
+ end
52
+
53
+ class Stage
54
+ attr_reader :resource
55
+ def initialize(pipeline, resource)
56
+ @pipeline = pipeline
57
+ @resource = resource
58
+ end
59
+
60
+ def committers
61
+ @pipeline.committers
62
+ end
63
+
64
+ def completed?
65
+ !building?
66
+ end
67
+
68
+ def unknown?
69
+ UNKNOWN_STATUS == @resource.stageStatus
70
+ end
71
+
72
+ # the stageStatus maybe 'Failed' when there is one job failed and others are still building
73
+ def building?
74
+ jobs.any? {|job| job.incompleted?}
75
+ end
76
+
77
+ def name
78
+ @resource.stageName
79
+ end
80
+
81
+ def counter
82
+ @resource.stageCounter
83
+ end
84
+
85
+ def to_s
86
+ "#{name}<#{counter}>[#{@pipeline}]"
87
+ end
88
+
89
+ def jobs
90
+ @jobs ||= @pipeline.find_jobs(self)
91
+ end
92
+ end
93
+
94
+ class Job
95
+ attr_reader :stage
96
+
97
+ def initialize(stage, resource)
98
+ @stage = stage
99
+ @resource = resource
100
+ end
101
+
102
+ def name
103
+ @resource.name
104
+ end
105
+
106
+ def passed?
107
+ @resource.result == PASSED_STATUS
108
+ end
109
+
110
+ def failed?
111
+ [FAILED_STATUS, CANCELED_STATUS].include? @resource.result
112
+ end
113
+
114
+ def completed?
115
+ @resource.is_completed == 'true'
116
+ end
117
+
118
+ def incompleted?
119
+ !completed?
120
+ end
121
+
122
+ def last_build_duration
123
+ @resource.last_build_duration
124
+ end
125
+
126
+ def current_build_duration
127
+ @resource.current_build_duration
128
+ end
129
+
130
+ def to_s
131
+ "Job #{name}[#{@stage}] #{completed? ? 'completed' : ''} #{failed? ? "failed" : (passed? ? 'passed' : 'unknown')}"
132
+ end
133
+ end
134
+
135
+ def initialize(pipelines)
136
+ @pipelines = pipelines
137
+ end
138
+
139
+ def each(&block)
140
+ @pipelines.each(&block)
141
+ end
142
+
143
+ def collect(&block)
144
+ @pipelines.collect(&block)
145
+ end
146
+ end
147
+
148
+ attr_reader :name, :resource
149
+ def initialize(resource)
150
+ @resource = resource
151
+ @name = resource.pipelineName
152
+ @pipeline = resource.groups.first
153
+ end
154
+
155
+ def stages
156
+ @pipeline.config.stages.collect do |stage|
157
+ Stage.new(stage, history)
158
+ end
159
+ end
160
+
161
+ def history
162
+ History.new(@pipeline.history.collect { | pipeline | History::Pipeline.new(self.name, pipeline) })
163
+ end
164
+ end
165
+ end
@@ -0,0 +1,16 @@
1
+ require 'rubygems'
2
+ require 'active_resource'
3
+
4
+ module CruiseFace
5
+ class Resource < ActiveResource::Base
6
+ def self.find_pipeline_status
7
+ find(:pipelineStatus)
8
+ end
9
+ def self.find_pipeline_history(name)
10
+ find(:pipelineHistory, :params => {:pipelineName => name})
11
+ end
12
+ def self.find_stage_status(params)
13
+ find(:stageStatus, :params => params)
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,166 @@
1
+ require 'ostruct'
2
+ require 'builder'
3
+
4
+ module CruiseFace::Model
5
+ class Stage
6
+
7
+ # History of a Stage cares about completed jobs, which maybe inside a running Stage
8
+ class History
9
+ def initialize(stages)
10
+ @stages = stages
11
+ end
12
+
13
+ def first
14
+ @stages.first
15
+ end
16
+
17
+ def passed?
18
+ as_jobs.all?(&:passed?)
19
+ end
20
+ def failed?
21
+ !passed?
22
+ end
23
+
24
+ def status
25
+ passed? ? 'Passed' : 'Failed'
26
+ end
27
+
28
+ def failed_jobs_from_now_until_passed(job_name)
29
+ jobs = []
30
+ @stages.each do |stage|
31
+ if job = stage.jobs.select(&:completed?).detect {|job| job.name == job_name}
32
+ if job.failed?
33
+ jobs << job
34
+ else
35
+ break
36
+ end
37
+ end
38
+ end
39
+ jobs
40
+ end
41
+
42
+ # A History of Stage could transform to a group of jobs, which is containing latest
43
+ # completed status in a Job's history.
44
+ # In some cases, the job may not have completed status, then the running/canceled job
45
+ # would be included in the return jobs
46
+ def as_jobs
47
+ @jobs ||= find_latest_completed_jobs
48
+ end
49
+
50
+ private
51
+ def find_latest_completed_jobs
52
+ return [] if first.nil?
53
+
54
+ latest_completed_jobs = []
55
+ @stages.each do |stage|
56
+ latest_completed_jobs = stage.jobs.collect do |job|
57
+ latest_completed_jobs.select(&:completed?).detect{|j| j.name == job.name} || job
58
+ end
59
+ if latest_completed_jobs.all?(&:completed?)
60
+ break
61
+ end
62
+ end
63
+
64
+ latest_completed_jobs
65
+ end
66
+ end
67
+
68
+ attr_reader :history
69
+
70
+ def initialize(config, pipelines)
71
+ @config = config
72
+ @history = History.new(pipelines.collect { |pipeline| pipeline.stages[name] }.reject {|history_stage| history_stage.unknown? })
73
+ end
74
+
75
+ def name
76
+ @config.name
77
+ end
78
+
79
+ def manual?
80
+ @config.isAutoApproved == 'false'
81
+ end
82
+
83
+ def building?
84
+ return false if no_history?
85
+ @history.first.building?
86
+ end
87
+
88
+ def building_jobs
89
+ return [] if no_history?
90
+ @history.first.jobs.select(&:incompleted?)
91
+ end
92
+
93
+ def latest_committers
94
+ return ['Unknown'] if no_history?
95
+ @history.first.committers
96
+ end
97
+
98
+ def no_history?
99
+ @history.first.blank?
100
+ end
101
+
102
+ def to_xml(xm)
103
+ xm.stage(:name => self.name) do
104
+ xm.latest(:status => self.history.status) do
105
+ xm.failed(self.history.failed?, :type => 'boolean')
106
+ xm.committers(:type => 'array') do
107
+ self.latest_committers.each { |committer| xm.committer(:name => committer) }
108
+ end
109
+ end
110
+ xm.building_jobs(:type => 'array') do
111
+ self.building_jobs.each { |job| xm.job(:name => job.name) }
112
+ end
113
+ xm.failing_jobs(:type => 'array') do
114
+ self.history.as_jobs.select(&:failed?).each do |job|
115
+ xm.job(:name => job.name) do
116
+ xm.committers(:type => 'array') { find_broking_builds_committers(job).each { |committer| xm.committer(:name => committer) } }
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end
122
+
123
+ def find_broking_builds_committers(failed_job)
124
+ broken_jobs = []
125
+ self.history.failed_jobs_from_now_until_passed(failed_job.name).each do |job|
126
+ if job.failed?
127
+ broken_jobs << job
128
+ else
129
+ break
130
+ end
131
+ end
132
+ broken_jobs.collect(&:stage).collect(&:committers).flatten.uniq
133
+ end
134
+
135
+ # from 0 to 100, as 0% to 100%
136
+ # when 1 building job
137
+ # => (building job current_build_duration/last_build_duration) * 100
138
+ # when 2 building job
139
+ # time left = [(job2.last_build_duration - job2.current_build_duration), (job1.last_build_duration - job1.current_build_duration)].max
140
+ # time built = [job2.current_build_duration, job1.current_build_duration].max
141
+ # => (time left / (time left + time built)) * 100
142
+ # when 1 building job1, 1 scheduled job2
143
+ # time left = job2.last_build_duration + (job1.last_build_duration - job1.current_build_duration)
144
+ # time built = job1.current_build_duration
145
+ # => (time left / (time left + time built)) * 100
146
+ # when 0 building job, 1 scheduled job
147
+ # => 0
148
+ # when 0 building job, 1 completed job1, 1 scheduled job2
149
+ # time left = job2.last_build_duration
150
+ # time built = job1.current_build_duration
151
+ # => (time left / (time left + time built)) * 100
152
+ # when 0 building job, 2 completed job1 & job2, 1 scheduled job3
153
+ # time left = job3.last_build_duration
154
+ # time built = job1.current_build_duration + job2.current_build_duration
155
+ # => (time left / (time left + time built)) * 100
156
+ # when 1 building job0, 2 completed job1 & job2, 1 scheduled job3
157
+ # time left = job3.last_build_duration + (job0.last_build_duration - job0.current_build_duration)
158
+ # time built = job1.current_build_duration + job2.current_build_duration + job0.current_build_duration
159
+ # => (time left / (time left + time built)) * 100
160
+ def progress
161
+ return 100 unless @history.first
162
+ completed_size = @history.first.jobs.select(&:completed?).size
163
+ completed_size * 100 / @history.first.jobs.size
164
+ end
165
+ end
166
+ end
data/lib/cruiseface.rb ADDED
@@ -0,0 +1 @@
1
+ require 'cruise_face'
data/rakefile ADDED
@@ -0,0 +1,60 @@
1
+
2
+ $LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__) + '/lib')
3
+ require 'cruise_face'
4
+
5
+ require 'logger'
6
+
7
+ ActiveResource::Base.logger = Logger.new(STDOUT)
8
+ ActiveResource::Base.logger.level = Logger::INFO
9
+
10
+ namespace :dump do
11
+ task :pipeline_status do
12
+ CruiseFace.site('https://cruise01.thoughtworks.com/cruise').login('xli', Base64.decode64(ENV['LDAP_CODE']))
13
+ ps = CruiseFace::Resource.find_pipeline_status
14
+ File.open "./tast/data/ps.xml", 'w' do |f|
15
+ f.write ps.to_xml
16
+ end
17
+ end
18
+
19
+ task :pipeline_history do
20
+ CruiseFace.site('https://cruise01.thoughtworks.com/cruise').login('xli', Base64.decode64(ENV['LDAP_CODE']))
21
+ pipeline = CruiseFace.get(ENV['PN'])
22
+ File.open "./tast/data/#{pipeline.name}.xml", 'w' do |f|
23
+ f.write pipeline.resource.to_xml
24
+ end
25
+ pipeline.history.each do |history_pipeline|
26
+ history_pipeline.stages.each do |stage_name, stage|
27
+ begin
28
+ stage_status = history_pipeline.find_stage_status(stage)
29
+ File.open "./tast/data/#{history_pipeline.name}-#{history_pipeline.label}-#{stage.name}-#{stage.counter}.xml", 'w' do |f|
30
+ f.write stage_status.to_xml
31
+ end
32
+ rescue => e
33
+ #ignore cruise stage not exist problem when the pipeline is building
34
+ end
35
+ end
36
+ end
37
+ puts 'done'
38
+ end
39
+ task :stage_status do
40
+ CruiseFace.site('https://cruise01.thoughtworks.com/cruise').login('xli', Base64.decode64(ENV['LDAP_CODE']))
41
+ pipeline = CruiseFace::Resource.find_stage_status(ENV['PN'])
42
+ File.open "./tast/data/#{pipeline.name}.xml", 'w' do |f|
43
+ f.write pipeline.resource.to_xml
44
+ end
45
+ puts 'done'
46
+ end
47
+ end
48
+
49
+ require 'echoe'
50
+
51
+ Echoe.new('cruiseface', '1.0.1') do |p|
52
+ p.description = "CruiseFace arms to give a friendly Cruise Pipeline dashboard info for developers."
53
+ p.url = "https://github.com/xli/cruiseface"
54
+ p.author = "Li Xiao"
55
+ p.email = "iam@li-xiao.com"
56
+ p.ignore_pattern = "*.gemspec"
57
+ p.dependencies = ["activeresource", "highline"]
58
+ p.ignore_pattern = ['tast/*.rb', 'tast/**/*']
59
+ p.rdoc_options = %w(--main README.rdoc --inline-source --line-numbers --charset UTF-8)
60
+ end
metadata ADDED
@@ -0,0 +1,99 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: cruiseface
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Li Xiao
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2010-03-12 00:00:00 -08:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: activeresource
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: "0"
24
+ version:
25
+ - !ruby/object:Gem::Dependency
26
+ name: highline
27
+ type: :runtime
28
+ version_requirement:
29
+ version_requirements: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: "0"
34
+ version:
35
+ description: CruiseFace arms to give a friendly Cruise Pipeline dashboard info for developers.
36
+ email: iam@li-xiao.com
37
+ executables:
38
+ - cruiseface
39
+ extensions: []
40
+
41
+ extra_rdoc_files:
42
+ - README.rdoc
43
+ - TODO
44
+ - bin/cruiseface
45
+ - lib/cruise_face.rb
46
+ - lib/cruise_face/console.rb
47
+ - lib/cruise_face/pipeline.rb
48
+ - lib/cruise_face/resource.rb
49
+ - lib/cruise_face/stage.rb
50
+ - lib/cruiseface.rb
51
+ files:
52
+ - Manifest
53
+ - README.rdoc
54
+ - TODO
55
+ - bin/cruiseface
56
+ - lib/cruise_face.rb
57
+ - lib/cruise_face/console.rb
58
+ - lib/cruise_face/pipeline.rb
59
+ - lib/cruise_face/resource.rb
60
+ - lib/cruise_face/stage.rb
61
+ - lib/cruiseface.rb
62
+ - rakefile
63
+ - Rakefile
64
+ - cruiseface.gemspec
65
+ has_rdoc: true
66
+ homepage: https://github.com/xli/cruiseface
67
+ licenses: []
68
+
69
+ post_install_message:
70
+ rdoc_options:
71
+ - --main
72
+ - README.rdoc
73
+ - --inline-source
74
+ - --line-numbers
75
+ - --charset
76
+ - UTF-8
77
+ require_paths:
78
+ - lib
79
+ required_ruby_version: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - ">="
82
+ - !ruby/object:Gem::Version
83
+ version: "0"
84
+ version:
85
+ required_rubygems_version: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: "1.2"
90
+ version:
91
+ requirements: []
92
+
93
+ rubyforge_project: cruiseface
94
+ rubygems_version: 1.3.5
95
+ signing_key:
96
+ specification_version: 3
97
+ summary: CruiseFace arms to give a friendly Cruise Pipeline dashboard info for developers.
98
+ test_files: []
99
+