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