Skip to content

Instantly share code, notes, and snippets.

@Brunas
Last active August 29, 2015 14:18
Show Gist options
  • Save Brunas/d5b9352b839b6249e372 to your computer and use it in GitHub Desktop.
Save Brunas/d5b9352b839b6249e372 to your computer and use it in GitHub Desktop.
TFS Build using ODataTFS

Description

This is Dashing widget to display TFS build progress. This was inspired and based on Jenkins Build.

The widget supports caching to avoid overlapping of several the same ODataTFS requests. Rufus scheduler version used in Dashing is too old and does not support :overlap => false. Cache file is /assets/config/.tfs_build_request_cache.json.

##Usage

The widget requires ODataTFS modified by me installed on some machine with access to TFS 2012 server.

Put the tfs_build.rb file in your /jobs folder. tfs_build.coffee, tfs_build.html and tfs_build.scss to /widgets/tfs_build. Put the tfs_project_mapping.json to /assets/config. Make sure that directory is accessible by Dashing user.

To include the widget in a dashboard, add the following snippet to the dashboard layout file:

<li data-row="1" data-col="1" data-sizex="1" data-sizey="1">
  <div data-id="TProject1" data-view="TFSBuild" data-title="Project1" data-description="Project1 - Main CI" data-min="0" data-max="100" data-time-offset="2" data-date-time-format="%Y-%m-%d %H:%M:%S"></div>
</li>

##Settings

Amend /assets/config/tfs_project_mapping.json to specify as many TFS projects with build definitions as you wish. Scheduler part of settings define 'every' and 'timeout' values for specific project's scheduler job. If there are many builds, it's better to have larger intervals to avoid performance degradation of ODataTFS service.

Change ODATA_URL value in /jobs/tfs_build.rb to point to ODataTFS service.

Changing settings requires service restart.

class Dashing.TFSBuild extends Dashing.Widget
@accessor 'value', Dashing.AnimatedValue
@accessor 'bgColor', ->
if @get('currentResult') == "Succeeded"
"#96bf48"
else if @get('currentResult') == "PartiallySucceeded"
"#F2C253"
else if @get('currentResult') == "Failed"
"#D26771"
else
"#999"
@accessor 'bgIcon', ->
if @get('currentResult') == "Succeeded"
"icon-thumbs-up"
else if @get('currentResult') == "PartiallySucceeded"
"icon-warning-sign"
else if @get('currentResult') == "Failed"
"icon-thumbs-down"
else if @get('currentResult') == "Stopped"
"icon-stop"
else
""
constructor: ->
super
@observe 'value', (value) ->
$(@node).find(".tfs-build").val(value).trigger('change')
ready: ->
@handleOffset()
$(@node).fadeOut().css('background-color', @get('bgColor'))
@handleBgIcon()
@handleMeterAndTitle()
$(@node).fadeIn()
onData: (data) ->
@handleOffset()
$(@node).fadeOut().css('background-color', @get('bgColor'))
@handleBgIcon()
@handleMeterAndTitle()
$(@node).fadeIn()
handleBgIcon: ->
bgIcon = $(@node).find(".icon-background")
if @get('bgIcon') != ''
$(@node).prepend("<i class='"+@get('bgIcon')+" icon-background' style='opacity:0.3;'></i>") if bgIcon.length < 1
else
bgIcon.remove()
handleMeterAndTitle: ->
meter = $(@node).find(".tfs-build")
title = $(@node).find(".title")
meter.attr("data-bgcolor", meter.css("background-color"))
meter.attr("data-fgcolor", meter.css("color"))
if @get('currentResult') isnt "InProgress"
meter.hide()
title.addClass("static-title")
else
title.removeClass("static-title")
meter.show()
meter.knob()
handleOffset: ->
offset = $(@node).data('time-offset')
nTime = new Date()
nTime.setTime(@get('timestamp2'))
@set('timestamp', @formatDateTime(nTime)) if !isNaN(nTime)
formatDateTime: (dateTime) ->
format = $(@node).data('date-time-format')
if format
format.replace('%Y',dateTime.getFullYear()).replace('%m', @zeroLPad(dateTime.getMonth()+1)).replace('%d',@zeroLPad(dateTime.getDate())).replace('%H',@zeroLPad(dateTime.getHours())).replace('%M', @zeroLPad(dateTime.getMinutes())).replace('%S', @zeroLPad(dateTime.getSeconds()))
else
dateTime.toLocaleDateString()+" "+dateTime.toLocaleTimeString()
zeroLPad: (i) ->
if i < 10 then "0" + i else i
<h2 class="title" data-bind="title"></h2>
<input class="tfs-build" data-angleOffset=-125 data-angleArc=250 data-width=250 data-readOnly=true data-bind-value="value | shortenedNumber" data-bind-data-min="min" data-bind-data-max="max">
<p class="more-info" data-bind="description"></p>
<p class="requested-for" data-bind="requestedFor"></p>
<p class="last-built" data-bind="timestamp"></p>
<p class="updated-at" data-bind="updatedAtMessage"></p>
require 'net/http'
require 'json'
require 'time'
require 'chronic'
class TFSBuild
ODATA_URL = URI.parse('http://127.0.0.1:8080')
READ_TIMEOUT = 120 #seconds
DEBUG = 0
MAPPING_FILE = 'assets/config/tfs_project_mapping.json'
CACHE_FILE = 'assets/config/.tfs_build_request_cache.json'
@@project_mapping = JSON.parse(IO.read(MAPPING_FILE))
#used to solve odata DDoS'ing problem until new rufus-sheduler
@@cache = {}
def debug
DEBUG
end
def project_mapping
@@project_mapping
end
# function to validate json
def valid_json? (json)
JSON.parse(json)
return true
rescue JSON::ParserError
return false
end
def parse_milliseconds(datestring)
datestring.scan(/[0-9]+/)[0].to_i
end
def parse_date(datestring)
return Time.at(parse_milliseconds(datestring)/1000)
end
def get_tfs_project_build_percentage(build_infos)
build_info = build_infos[0]
prev_build_info = build_infos[1]
return 0 if build_info["BuildFinished"]
last_duration = ((prev_build_info["FinishTime"].scan(/[0-9]+/)[0].to_i - prev_build_info["StartTime"].scan(/[0-9]+/)[0].to_i) / 1000).round(0)
current_duration = (Time.now.to_i - (build_info["StartTime"].scan(/[0-9]+/)[0].to_i / 1000)).round(0)
return 99 if current_duration >= last_duration
result = ((current_duration * 100) / last_duration).round(0)
return 0 if result < 0
result
end
def get_start_date_for_filter
Chronic.parse('6 months ago').strftime('%Y-%m-%dT00:00:00')
end
def get_active_request_count
return 0 if not @@cache
@@cache.count
end
def insert_to_cache(id)
@@cache[id] = 1
IO.write(CACHE_FILE, @@cache.to_json)
end
def is_in_cache?(id)
@@cache.has_key?(id)
end
def remove_from_cache(id)
@@cache.delete(id) if @@cache.has_key?(id)
IO.write(CACHE_FILE, @@cache.to_json)
end
def get_json_for_tfs_project(id, project_name, build_definition, top = 1, record_to_return = -1)
return if is_in_cache?(id) or @@cache.count > @@project_mapping.count / 2
insert_to_cache(id)
url = URI.encode("/Projects('#{project_name}')/Builds?$format=json&$orderby=StartTime desc&$top=#{top}&$select=Definition,RequestedFor,LastChangedOn,Status,StartTime,FinishTime&$filter=Definition eq '#{build_definition}' and Status ne 'Stopped' and StartTime gt datetime'#{get_start_date_for_filter}'")
puts url if DEBUG > 2
http = Net::HTTP.new(ODATA_URL.host, ODATA_URL.port)
http.read_timeout = READ_TIMEOUT
request = Net::HTTP::Get.new(url)
response = http.request(request)
if not valid_json?(response.body)
remove_from_cache(id)
return
end
json = JSON.parse(response.body)
puts json if DEBUG > 2
remove_from_cache(id)
return json['d']['results'][record_to_return] if json and json['d'] and json['d']['results'] and record_to_return > -1
return json['d']['results'] if json and json['d'] and json['d']['results']
rescue Timeout::Error
puts DateTime.now.to_s+' Timeout occured while requesting '+url
remove_from_cache(id)
rescue Errno::ECONNRESET => e
puts DateTime.now.to_s+' Connection reset by peer exception occured while requesting '+url
remove_from_cache(id)
end
end
@TFSBuild = TFSBuild.new()
@LastActiveRequests = 0
@TFSBuild.project_mapping.each do |title, project|
current_status = nil
scheduler_config = project['scheduler'] if project['scheduler']
scheduler_config = ['every' => '120s', 'timeout' => '300s'] if not project['scheduler']['every'] or not project['scheduler']['timeout']
every = scheduler_config['every']
timeout = scheduler_config['timeout']
SCHEDULER.every every, first_in: 0, timeout: timeout do |prj|
last_status = current_status
reqStarted = DateTime.now
build_infos = @TFSBuild.get_json_for_tfs_project(title, project['project'], project['build_definition'], 2)
next if not build_infos or not build_infos.kind_of?(Array)
reqEnded = DateTime.now
puts build_infos if @TFSBuild.debug > 2
build_info = build_infos[0]
next if not build_info
puts reqStarted.strftime('%F %T') + '-'+reqEnded.strftime('%F %T')+' ('+(reqEnded.to_time.to_i-reqStarted.to_time.to_i).to_s+'s):' if @TFSBuild.debug > 0
puts ' '+build_info['Definition']+' '+build_info["RequestedFor"]+' '+build_info['LastChangedOn']+' '+@TFSBuild.parse_date(build_info["LastChangedOn"]).strftime('%F %T') if @TFSBuild.debug > 1
puts ' '+build_infos[1]['Definition']+' '+build_infos[1]["RequestedFor"]+' '+build_infos[1]['LastChangedOn']+' '+@TFSBuild.parse_date(build_infos[1]["LastChangedOn"]).strftime('%F %T') if @TFSBuild.debug > 1 and build_infos[1]
next if not build_info['Status']
current_status = build_info['Status']
if current_status == 'InProgress'
percent = @TFSBuild.get_tfs_project_build_percentage(build_infos)
end
send_event(title, {
currentResult: current_status,
lastResult: last_status,
timestamp: @TFSBuild.parse_date(build_info["LastChangedOn"]).strftime('%F %T'),
timestamp2: @TFSBuild.parse_milliseconds(build_info["LastChangedOn"]),
value: percent,
description: build_info["Definition"],
buildStarted: @TFSBuild.parse_date(build_info["StartTime"]).strftime('%F %T'),
buildFinished: @TFSBuild.parse_date(build_info["FinishTime"]).strftime('%F %T'),
requestedFor: build_info["RequestedFor"]
})
currentActiveRequests = @TFSBuild.get_active_request_count
send_event('TFSBuildActiveRequestCount', { current: currentActiveRequests, last: @LastActiveRequests})
@LastActiveRequests = currentActiveRequests
end
end
// ----------------------------------------------------------------------------
// Sass declarations
// ----------------------------------------------------------------------------
$background-color: #4b5f24;
$title-color: rgba(255, 255, 255, 1);
$moreinfo-color: rgba(255, 255, 255, 0.7);
$requestedfor-color: #000; /*rgba(255, 255, 255, 1);*/
$lastbuilt-color: #000; /*rgba(255, 255, 255, 1);*/
$meter-background: rgba(20, 20, 20, 1);
// ----------------------------------------------------------------------------
// Widget-tfs-build styles
// ----------------------------------------------------------------------------
.widget-tfs-build {
background-color: $background-color;
padding-top: 0px !important;
input.tfs-build {
background-color: $meter-background;
color: #fff;
text-shadow:
-2px -2px 0 #000,
2px -2px 0 #000,
-2px 2px 0 #000,
2px 2px 0 #000;
}
.title {
color: $title-color;
font-size: 290%;
text-align:center;
text-shadow:
-2px -2px 0 #000,
2px -2px 0 #000,
-2px 2px 0 #000,
2px 2px 0 #000;
}
.static-title {
font-size:370%;
}
.more-info {
color: $moreinfo-color;
top:10px;
font-size:+1em;
}
.requested-for {
font-size: 20px;
font-weight: bold;
position: absolute;
bottom: 57px;
left: 0;
right: 0;
color: $requestedfor-color;
}
.last-built {
font-size: 20px;
font-weight: bold;
position: absolute;
bottom: 37px;
left: 0;
right: 0;
color: $lastbuilt-color;
}
.updated-at {
color: rgba(0, 0, 0, 1);
}
}
{"TProject1":{"project":"Project1 Databases","build_definition":"Project1 - Main CI","scheduler":{"every":"127s","timeout":"4m"}},"TSuperDuper":{"project":"Super Duper","build_definition":"Super Duper Nightly","scheduler":{"every":"125s","timeout":"3m"}}}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment