Blog Stats
  • Posts - 60
  • Articles - 0
  • Comments - 6
  • Trackbacks - 0

 

Tuesday, July 8, 2014

TFS 2013 Build Process Template – Change the “AutomatedTest” Argument Default Value


It should be simple, but due to the length of the string, the syntax was not obvious. The out-of-the-box default is as follows. Note the backslash escape character and stick to the syntax:

{New Microsoft.TeamFoundation.Build.Common.BuildParameter(" { ""AssemblyFileSpec"": ""**\\*test*.dll;**\\*test*.appx"", ""RunSettingsFileName"": null, ""TestCaseFilter"": """", ""RunSettingsForTestRun"": { ""ServerRunSettingsFile"": """", ""TypeRunSettings"": ""Default"", ""HasRunSettingsFile"": false }, ""HasRunSettingsFile"": false, ""HasTestCaseFilter"": false, ""ExecutionPlatform"": ""X86"", ""FailBuildOnFailure"": false, ""RunName"": """" } ")}

If you remove the values to expose the syntax you get:

{New Microsoft.TeamFoundation.Build.Common.BuildParameter("")}

To add another “test” to the template use this:

{New Microsoft.TeamFoundation.Build.Common.BuildParameter(""), 
New Microsoft.TeamFoundation.Build.Common.BuildParameter("")}

and just provide the desired values. Here’s what I ended up with:

{New Microsoft.TeamFoundation.Build.Common.BuildParameter(" { ""AssemblyFileSpec"": ""*UnitTests\\*Tests.dll"", ""RunSettingsFileName"": null, ""TestCaseFilter"": """", ""RunSettingsForTestRun"": { ""ServerRunSettingsFile"": """", ""TypeRunSettings"": ""Default"", ""HasRunSettingsFile"": false }, ""HasRunSettingsFile"": false, ""HasTestCaseFilter"": false, ""ExecutionPlatform"": ""X64"", ""FailBuildOnFailure"": false, ""RunName"": ""Unit"" } "), New Microsoft.TeamFoundation.Build.Common.BuildParameter(" { ""AssemblyFileSpec"": ""*IntegrationTests\\*Tests.dll"", ""RunSettingsFileName"": null, ""TestCaseFilter"": """", ""RunSettingsForTestRun"": { ""ServerRunSettingsFile"": """", ""TypeRunSettings"": ""Default"", ""HasRunSettingsFile"": false }, ""HasRunSettingsFile"": false, ""HasTestCaseFilter"": false, ""ExecutionPlatform"": ""X64"", ""FailBuildOnFailure"": false, ""RunName"": ""Integration"" } ")}

Nuget Push to Klondike Requires Packages Appended to Source and SetApiKey URL


When we upgraded to NuGet 2.8 our PUSH command  starting failing.  The first fix was to appended “packages” to the source URL string like this:

myFeed.com:8080/api/packages

We use the SetApiKey command on our server so we don’t have to explicitly include the API key itself in the push command. After making the change above we had issues with the PUSH command not finding the API key. To fix this, we appended “packages” to the source argument of the SetApiKey command like this:

nuget setApiKey myApiKey -Source myFeed.com:8080/api/packages

Everything then worked fine as before.

Wednesday, June 25, 2014

Get the Last Build By Build Number with Visual Studio ALM Team Foundation Server API


Here is a handy TFS 2013 build activity for getting the last build by build number filter. In this case:

  • We get the 4th version node number: we call it the revision number
  • We use semantic versioning and some of our branches are “version named” i.e. 1.4.10
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Activities;
using System.IO;
using System.Text.RegularExpressions;
using Microsoft.TeamFoundation.Build.Client;
using Microsoft.TeamFoundation.Client;
using Microsoft.TeamFoundation.Build.Workflow;

namespace Alm.BuildActivities
{
[BuildActivity(HostEnvironmentOption.Agent)]
    public sealed class GetVersionNamedBranchLastRevisionNumber : CodeActivity
    {
        #region arguments
        [RequiredArgument]
        public InArgument<string> BuildDefinitionName { get; set; }
        public InArgument<string> CollectionUrl { get; set; }
        public InArgument<string> ProjectName { get; set; }
        public InArgument<string> VersionNamedBranch { get; set; }
        public OutArgument<int> CurrentRevisionNumber { get; set; }
        #endregion

        protected override void Execute(CodeActivityContext context)
        {
            #region initialize fields
            context.SetValue(this.CurrentRevisionNumber, 0);
            string buildDefinitionName = context.GetValue(this.BuildDefinitionName);
            string collectionUrl = context.GetValue(this.CollectionUrl);
            string projectName = context.GetValue(this.ProjectName);
            string versionNamedBranch = context.GetValue(this.VersionNamedBranch);
            int currentRevisionNumber = 0;
            #endregion

            try
            {
                TfsTeamProjectCollection tpc = new TfsTeamProjectCollection(new Uri(collectionUrl));
                tpc.EnsureAuthenticated();

                if (!(tpc == null))
                {
                    var bs = tpc.GetService<IBuildServer>();
                    var buildDefSpec = bs.CreateBuildDefinitionSpec(projectName, buildDefinitionName);
                    buildDefSpec.PropertyNameFilters.Add("Name");
                    buildDefSpec.Options = QueryOptions.None;
                    var buildDef = bs.QueryBuildDefinitions(buildDefSpec).Definitions.FirstOrDefault();

                    var buildSpec = bs.CreateBuildDetailSpec(buildDef);
                    //gets only "stable builds" of the version named branch
                    buildSpec.BuildNumber = string.Format("*_{0}.?", versionNamedBranch); 
                    buildSpec.QueryDeletedOption = QueryDeletedOption.IncludeDeleted;
                    buildSpec.MaxBuildsPerDefinition = 1;
                    //gets last stable build of the version named branch
                    buildSpec.QueryOrder = BuildQueryOrder.StartTimeDescending; 

                    var buildQuery = bs.QueryBuilds(buildSpec).Builds.FirstOrDefault();

                    if (buildQuery == null)
                    {
                        currentRevisionNumber = 0; //there aren no stable builds of this version named branch
                        context.SetValue(this.CurrentRevisionNumber, currentRevisionNumber);
                    }
                    else
                    {
                        var lastStableBuildNumber = buildQuery.BuildNumber;
                        string currentRevisionNumberString = lastStableBuildNumber
                            .Substring(lastStableBuildNumber.LastIndexOf(".") + 1);
                        bool result = Int32.TryParse(currentRevisionNumberString, out currentRevisionNumber);
                        if (result)
                        {
                            context.SetValue(this.CurrentRevisionNumber, currentRevisionNumber); 
                        }
                        else
                        {
                            string text = "ERROR: unable to parse to int the currentRevisionNumberString value:";
                            throw new Exception(string.Format("{0} {1}.", text, currentRevisionNumberString));
                        }
                    }
                }
                else
                {
                    throw new Exception("ERROR: unable to connect to TFS collection");
                }
            }
            catch (Exception ex)
            {
                throw new Exception(string.Format("ERROR. Exception message: {0}", ex.Message));
            }
        }
    }
}

Monday, April 28, 2014

Delete Build Drop Locations Some Number of Days Old


We don’t need to keep our drop folders after a week. Here some code to accomplish that.

[TestMethod]
public void SetDeleteBuildDropFolder()
{
    //method parms
    string collectionUrl = "http://myCollectionUrl";
    string projectName = "myTeamProjectName";
    var buildDefinitionNames = new List<string>();
    buildDefinitionNames.Add("myBuildDefinitionName");
    
    var fromDate = DateTime.Now.AddDays(-8);
    
    //execute method
    var buildsDropFoldersDeleted = tfsApi.SetDeleteBuildDropFolder(collectionUrl, projectName, buildDefinitionNames, fromDate);

    //evaluate result
    Assert.IsNotNull(buildsDropFoldersDeleted);
}
        
public Dictionary<string, string> SetDeleteBuildDropFolder(string collectionUrl, string projectName, 
List<string> buildDefinitionNames, DateTime fromDateTime) { var buildsDropFoldersDeleted = new Dictionary<string, string>(); try { TfsTeamProjectCollection tpc = GetCollection(collectionUrl); if (!(tpc == null)) { var bs = tpc.GetService<IBuildServer>(); foreach (var buildDefName in buildDefinitionNames) { var buildSpec = bs.CreateBuildDetailSpec(projectName, buildDefName); buildSpec.QueryOptions = QueryOptions.None; buildSpec.InformationTypes = null; buildSpec.MaxBuildsPerDefinition = 100; buildSpec.QueryOrder = BuildQueryOrder.StartTimeDescending; buildSpec.QueryDeletedOption = QueryDeletedOption.ExcludeDeleted; if (buildSpec != null) { var buildsDrops = bs.QueryBuilds(buildSpec).Builds.Where(x => x.FinishTime < fromDate)
.Select(y => new { y.Uri, y.DropLocation }); foreach (var build in buildsDrops) { if (Directory.Exists(build.DropLocation)) { try { var bSpec = bs.CreateBuildDetailSpec(projectName, buildDefName); bSpec.QueryOptions = QueryOptions.None; bSpec.InformationTypes = null; bSpec.QueryDeletedOption = QueryDeletedOption.ExcludeDeleted; //Using the build service you can delete multiple build drop folders at the same time //var targetBuild = bs.QueryBuilds(bSpec).Builds.Where(b => b.Uri == build.Uri).ToArray(); //bs.DeleteBuilds(targetBuild, DeleteOptions.DropLocation); //Here I'm only deleting a single build drop folder at a time var targetBuild = bs.QueryBuilds(bSpec).Builds.Where(b => b.Uri == build.Uri).FirstOrDefault(); if (targetBuild.KeepForever) { targetBuild.KeepForever = false; targetBuild.Save(); } targetBuild.Delete(DeleteOptions.DropLocation); buildsDropFoldersDeleted.Add(targetBuild.BuildNumber, build.DropLocation); } catch (Microsoft.TeamFoundation.Build.Client.BuildDefinitionNotFoundException) { //build not found } } } } else { return null; } } return buildsDropFoldersDeleted; } else { return null; } } catch (Exception ex) { return null; } }

Monday, January 20, 2014

Formatting Command Output on the TFS Build Report


The Team Foundation Server build can include command output in the report log or summary by using the stdOutput and errOutput variables in the WriteBuildMessage or WriteCustomSummaryInformation activities. I’ve often been frustrated with bad formatting that you get.  However, I found that if you simply use String.Format with stdOutput or errOuput, you get much better formatting. For example, with String.Format("{0}", stdOutput) you get correct line returns.

Tuesday, January 14, 2014

Making Agile Work on Large, Complex Projects


Our TFS Austin User Group hosted this presentation last month given by Mike Haze, Director of Product Management for Volusion.  It was one of the best meetings we’ve had.  Here’s the abstract and a link to the recording of the meeting.

Recording Link: http://usergroup.tv/videos/making-agile-work-on-large-complex-projects 

Presentation: Making Agile Work on Large, Complex Projects

There is a widely accepted myth that extremely complex software platforms, committed delivery schedules, large development teams, and dynamic business environments are in direct opposition to agile development processes and tools and if your product / project fall into one of these categories you will struggle with fully embracing agile methodologies. This is simply not the case, you can fully adopt and leverage an agile methodology through out the full product lifecycle, gaining all the advantages and leverage the tools your teams are already using today and are tightly integrated into the development process. We will explore how Volusion is currently leveraging TFS throughout the complete product lifecycle and discuss some of the key challenges and successes that we've experienced along the way.

Speaker

Mike Haze is the Director of Product Management at Volusion. Mike holds a number of patents including "System and method for migration of digital assets" and "System and method for self-provisioning of virtual images." Beginning his career as a Software Developer, Mike has served as a Product Experience Strategist for Dell and the Director of Product Development and Innovation for Dun and Bradstreet.

Volusion is a leading e-commerce software development company based in Austin with $12B in merchant sales worldwide, over 40,000 online stores and over 450 employees. Volusion has won the Austin Business Journal's award for Best Places to Work for 2012 and 2013.

TfsUGMikeHaze1 TfsUGMikeHaze2

Wednesday, July 24, 2013

TFS 2012 API Create Alert Subscriptions


There were only a few post on this and I felt like really important information was left out:

  1. What the defaults are
  2. How to create the filter string

Here’s the code to create the subscription.

Get the Collection

public TfsTeamProjectCollection GetCollection(string collectionUrl)
        {
            try
            {
                //connect to the TFS collection using the active user
                TfsTeamProjectCollection tpc = new TfsTeamProjectCollection(new Uri(collectionUrl));
                tpc.EnsureAuthenticated();
                return tpc;
            }
            catch (Exception)
            {
                return null;
            }
        }

Use Impersonation

Because my app is used to create “support tickets” as stories in TFS, I use impersonation so the subscription is setup for the “requester.”  That way I can take all the defaults for the subscription delivery preferences.

public TfsTeamProjectCollection GetCollectionImpersonation(string collectionUrl, string impersonatingUserAccount)
        {
            // see: http://blogs.msdn.com/b/taylaf/archive/2009/12/04/introducing-tfs-impersonation.aspx
            try
            {
                TfsTeamProjectCollection tpc = GetCollection(collectionUrl);
                if (!(tpc == null))
                {
                    //get the TFS identity management service (v2 is 2012 only)
                    IIdentityManagementService2 ims = tpc.GetService<IIdentityManagementService2>();

                    //look up the user we want to impersonate
                    TeamFoundationIdentity identity = ims.ReadIdentity(IdentitySearchFactor.AccountName, 
                        impersonatingUserAccount, 
                        MembershipQuery.None, 
                        ReadIdentityOptions.None);

                    //create a new connection using the impersonated user account
                    //note: do not ensure authentication because the impersonated user may not have 
                    //windows authentication at execution
                    if (!(identity == null))
                    {
                        TfsTeamProjectCollection itpc = new TfsTeamProjectCollection(tpc.Uri, identity.Descriptor);
                        return itpc;
                    }
                    else
                    {
                        //the user account is not found
                        return null;
                    }
                }
                else
                {
                    return null;
                }
            }
            catch (Exception)
            {
                return null;
            }
        }

Create the Alert Subscription

public bool SetWiAlert(string collectionUrl, string projectName, int wiId, string emailAddress, string userAccount)
        {
            bool setSuccessful = false;
            try
            {
                //use impersonation so the event service creating the subscription will default to 
                //the correct account: otherwise domain ambiguity could be a problem
                TfsTeamProjectCollection itpc = GetCollectionImpersonation(collectionUrl, userAccount);

                if (!(itpc == null))
                {

                    IEventService es = itpc.GetService(typeof(IEventService)) as IEventService;

                    DeliveryPreference deliveryPreference = new DeliveryPreference();
                    //deliveryPreference.Address = emailAddress;
                    deliveryPreference.Schedule = DeliverySchedule.Immediate;
                    deliveryPreference.Type = DeliveryType.EmailHtml;

                    //the following line does not work for two reasons: 
                    //string filter = string.Format("\"ID\" = '{0}' AND \"Authorized As\" <> '[Me]'", wiId);

                    //1. the create fails because there is a space between Authorized As
                    //2. the explicit query criteria are all incorrect anyway
                    //   see uncommented line for what does work: you have to create the subscription mannually 
                    //   and then get it to view what the filter string needs to be (see following commented code)
                    
                    //this works
                    string filter = string.Format("\"CoreFields/IntegerFields/Field[Name='ID']/NewValue\" = '12175'" + 
                                                    " AND \"CoreFields/StringFields/Field[Name='Authorized As']/NewValue\"" + 
                                                    " <> '@@MyDisplayName@@'", projectName, wiId);

                    string eventName = string.Format("<PT N=\"ALM Ticket for Work Item {0}\"/>", wiId);

                    es.SubscribeEvent("WorkItemChangedEvent", filter, deliveryPreference, eventName);

                    ////use this code to get existing subscriptions: you can look at manually created 
                    ////subscriptions to see what the filter string needs to be
                    //IIdentityManagementService2 ims = itpc.GetService<IIdentityManagementService2>();
                    //TeamFoundationIdentity identity = ims.ReadIdentity(IdentitySearchFactor.AccountName, 
                    //    userAccount, 
                    //    MembershipQuery.None, 
                    //    ReadIdentityOptions.None);
                    //var existingsubscriptions = es.GetEventSubscriptions(identity.Descriptor);

                    setSuccessful = true;

                    return setSuccessful;
                }
                else
                {
                    return setSuccessful;
                }
            }
            catch (Exception)
            {
                return setSuccessful;
            }
        }

Thursday, May 30, 2013

Adding a user to the TFS "Project Collection Service Accounts" group


Some types of work item updates require the updating account to be in the “Project Collection Service Accounts” group. For example, the work item server - var wiSrv = tpc.GetService<WorkItemServer>();

You have to use the command line to do this. Here’s an example:

tfssecurity /g+ "Project Collection Service Accounts" n:{domain\user} /collection:{your collection url}

Wednesday, May 8, 2013

Semantic Versioning


In a discussion about software product versioning, the post on Semantic Versioning came up. I found the essay to reflect commonly recognized best practices. However, the value of the essay is in the concise and rigorous rules for incrementing version numbers. 

Monday, March 11, 2013

Retain and Set Posted Checkbox Value in the MVC 4 Controller


I believe this is a bug where only the checkbox value is not retained when passing the model from the view to a post method.

Model

public bool CoreField { get; set; }

View

@model List<Model>

@Html.CheckBoxFor(m => m[i].CoreField, 
 new { @id = "cbCoreField" + i })@Html.HiddenFor(m => m[i].CoreField)
@Html.Hidden("fIsCore", 
 null, new { @id = "hIsCore" + i }) 
//has to use separate list instead of model due to MVC bug

Note the use of “fIsCore” as a separate list used to pass changes to the checkbox into the post method and the use of ModelState.SetModelValue to set the checkbox value for redisplay when the model is invalid (i.e. adding a row where the model validation rules for some other field has been violated.

Controller Post Method

[HttpPost]
        public ActionResult Index(List<Model> model, List<bool> fIsCore)
        {
            if (!(model == null))
            {
                if (ModelState.IsValid)
                {
                    int i = 0;
                    foreach (var item in model)
                    {
                        item.CoreField = fIsCore[i];
                        i++;

                        if (!(item.ID == 0))
                        {
                            db.Entry(item).State = EntityState.Modified;
                        }
                        else
                        {
                            db.Model.Add(item);
                        }
                    }
                    db.SaveChanges();
                }
                else
                {
                    //ModelState.Clear(); // not needed now
                    int i = 0;
                    foreach (var item in model)
                    {
                        string key = string.Format("[{0}].CoreField", i);
                        item.CoreField = fIsCore[i];
                        ModelState.SetModelValue(key, 
                            new ValueProviderResult(item.CoreField, 
                                "",
                                CultureInfo.InvariantCulture));
                        i++;
                    }

                    return View(model);
                }
            }
            return RedirectToAction("Index");
        }
 

 

Copyright © Bob Hardister