cruiseface 1.0.1

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