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 +12 -0
- data/README.rdoc +72 -0
- data/Rakefile +60 -0
- data/TODO +17 -0
- data/bin/cruiseface +104 -0
- data/cruiseface.gemspec +38 -0
- data/lib/cruise_face.rb +44 -0
- data/lib/cruise_face/console.rb +187 -0
- data/lib/cruise_face/pipeline.rb +165 -0
- data/lib/cruise_face/resource.rb +16 -0
- data/lib/cruise_face/stage.rb +166 -0
- data/lib/cruiseface.rb +1 -0
- data/rakefile +60 -0
- metadata +99 -0
data/Manifest
ADDED
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
|
data/cruiseface.gemspec
ADDED
@@ -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
|
data/lib/cruise_face.rb
ADDED
@@ -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
|
+
|