Query TFS/VSTS for past build history

April 20, 2017    TFS/VSTS/AzureDevOps DevOps TFS

Query TFS/VSTS for past build history

We’re using TFS on premise (2017, not an update) to run builds, track work, etc at my current client. This should also work on VSTS (which I hope we will get to move towards someday).

I was asked to figure out why our build times have increased over the last couple months. My first task was to get the metrics. After not finding the graph I wanted in the charts in the TFS dashboard, I turned to the TFS API.

I used this starting point from the Microsoft docs .

There was also some info from StackOverflow that was helpful.

I created a plain old .Net Console application to spit out a .csv file.

I added a few Nuget packages

  • Microsoft.TeamFoundationServer.Client
  • Microsoft.VisualStudio.Services.Client
  • Microsoft.VisualStudio.Services.InteractiveClient

Then I added some code

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.TeamFoundation.Build.WebApi;
using Microsoft.VisualStudio.Services.Client;
using Microsoft.VisualStudio.Services.WebApi;`

namespace GetBuildHistory
{
    class Program
    {
        static void Main(string[] args)
        {
            MainAsync().Wait();
        }

        static async Task MainAsync()
        {
            var collectionUri = "http://tfs:8080/collection/";
            var teamProjectName = "{Your_Project_Name}";
            var targetBuildName = "{Your_BUILD_Name}";

            VssConnection connection = new VssConnection(new Uri(collectionUri), new VssClientCredentials());
            var buildserver = connection.GetClient<BuildHttpClient>();
            var builds = await buildserver.GetBuildsAsync(
                statusFilter: BuildStatus.Completed,
                project: teamProjectName);
            var targetedBuilds = builds
                .Where(definition => definition.Definition.Name.Contains(targetBuildName))
                .OrderBy(b => b.FinishTime)
                .ToList();

            ProcessBuilds(targetedBuilds);
        }

        private static void ProcessBuilds(List<Build> targetedBuilds)
        {
            var pathString = $@"C:\results\general_fullBuildResults.csv";
            var stringBuilder = new StringBuilder();
            stringBuilder.AppendLine("Name, Date, Passed, Build Time, Queue Time");
            foreach (var build in targetedBuilds)
            {
                var buildInfo = new BuildInfo
                {
                    Name = build.BuildNumber,
                    Passed = build.Result != null && build.Result.Value == BuildResult.Succeeded,
                    FinishTime = build.FinishTime.Value,
                    Started = build.StartTime.Value,
                    QueueStartTime = build.QueueTime.Value
                };

                stringBuilder.AppendLine(buildInfo.ToString());
            }

            File.WriteAllText(pathString, stringBuilder.ToString());
        }
    }

    internal class BuildInfo
    {
        public string Name { private get; set; }
        public bool Passed { private get; set; }
        public DateTime Started { private get; set; }
        public DateTime FinishTime { private get; set; }
        public DateTime QueueStartTime { private get; set; }

        private string InQueueTimeMinutes => this.CalculateTimeInQueue();

        /// <summary>
        /// The time the build took to run.
        /// </summary>
        public string TimeRanInMinutes => this.CalculateTimeRan();

        private string CalculateTimeRan()
        {
            var diff = this.FinishTime - this.Started;
            return (diff.TotalSeconds / 60).ToString(CultureInfo.CurrentCulture);
        }

        private string CalculateTimeInQueue()
        {
            var diff = this.Started - this.QueueStartTime;
            return (diff.TotalSeconds / 60).ToString(CultureInfo.CurrentCulture);
        }

        public override string ToString()
        {
            return $"{this.Name}, {this.Started.ToLocalTime():g}, {this.Passed}, {this.TimeRanInMinutes}, {this.InQueueTimeMinutes}";
        }
    }
}

I will probably add to this, but this works for me. It spits out a csv that I can use Excel to create an easy chart. This could morph into a TFS extension chart (which would be great, but more work and time than I have right now). We could even run this automatically and alert if the build time goes over a certain threshold. That could mean we have problems (see the DevOps Handbook Chapter 14 on the importance of metrics).

I’m authenticating through my Windows Active Directory account, so the console didn’t prompt me. Check the docs link above for more information about that.

But wait, there’s more

We can even drill into the task level of the builds and use Power BI to visualize the timings there too.

 /// <summary>
    /// Get the timeline information from the REST API
    /// </summary>
    /// <remarks>
    /// reference of the REST API: https://www.visualstudio.com/en-us/docs/integrate/api/build/builds
    /// </remarks>
    /// <param name="buildId"></param>
    private static async Task<Timeline> GetTimelineAsync(int buildId)
    {
        Console.WriteLine($"Getting timeline for {buildId}....");
        return await buildServerClient.GetBuildTimelineAsync(teamProjectName, buildId);
    }

I added this to my BuildInfo.cs object and used it to create the CSV rows.

public string GetTimelineCsvRow()
{
    // Build Name, Date, Passed, Task Name, Time in Minutes, State, Result
    var sb = new StringBuilder();
    var orderedRecords = this.Timeline.Records
        .OrderBy(record => record.StartTime).ToList();

    orderedRecords.ForEach(record =>
    {
        try
        {

        sb.AppendLine($"{this.Name}, {record.StartTime.Value.ToLocalTime():g}, " +
                    $"{this.Passed}, {record.Name}, " +
                    $"{this.CalculateTimeRan(record.StartTime.Value, record.FinishTime.Value)}, ${record.State}, ${record.Result}, ${record.Url}");

        }
        catch (Exception e)
        {
            Console.WriteLine(e);
            sb.AppendLine("Exception");
        }
    });

    return sb.ToString();
}

Here’s an example of the .csv output. I’ve removed some steps to condense it.

Build Name Start Time Passed Task Name Time in Minutes State Result
1.0.662.0 6/27/2017 8:30 PM True Build 40.336 Completed Succeeded
1.0.662.0 6/27/2017 8:30 PM True Get sources 0.901833333 Completed Succeeded
1.0.662.0 6/27/2017 8:31 PM True Add Git Remote 0.020283333 Completed Succeeded
1.0.662.0 6/27/2017 8:31 PM True Run git pull 0.008566667 Completed Succeeded
1.0.662.0 6/27/2017 8:31 PM True npm install --cache-min 604800 1.1625 Completed Succeeded
1.0.662.0 6/27/2017 8:32 PM True NuGet restore Project.sln 0.280716667 Completed Succeeded
1.0.662.0 6/27/2017 8:32 PM True Build Project.sln 0.4289 Completed Succeeded
1.0.662.0 6/27/2017 8:33 PM True grunt build 1.751333333 Completed Succeeded
1.0.662.0 6/27/2017 8:34 PM True C# Tests 0.4266 Completed Succeeded
1.0.662.0 6/27/2017 8:35 PM True JS Unit Tests 1.151566667 Completed Succeeded
1.0.662.0 6/27/2017 8:36 PM True Publish Test Results **/*.trx 1.2375 Completed Succeeded
1.0.662.0 6/27/2017 8:38 PM True Build ProjectInstaller.sln 0.25755 Completed Succeeded
1.0.662.0 6/27/2017 8:40 PM True Copy files from buildScripts 0.781766667 Completed Succeeded
1.0.662.0 6/27/2017 8:41 PM True Run UI Install on AGENT-01 AGENT-02 AGENT-03 AGENT-04 1.622166667 Completed
1.0.662.0 6/27/2017 8:42 PM True Run Tests 15.54276667 Completed Succeeded
1.0.662.0 6/27/2017 9:05 PM True Run Cleanup on AGENT-01 AGENT-02 AGENT-03 AGENT-04 1.322283333 Completed
1.0.662.0 6/27/2017 9:06 PM True Publish symbols path: \Symbols 2.599833333 Completed Succeeded
1.0.662.0 6/27/2017 9:10 PM True Label sources 0.003383333 Completed Succeeded



Watch the Story for Good News
I gladly accept BTC Lightning Network tips at [email protected]

Please consider using Brave and adding me to your BAT payment ledger. Then you won't have to see ads! (when I get to $100 in Google Ads for a payout (I'm at $97.66!), I pledge to turn off ads)

Use Brave

Also check out my Resources Page for referrals that would help me.


Swan logo
Use Swan Bitcoin to onramp with low fees and automatic daily cost averaging and get $10 in BTC when you sign up.