Make easy storing secure password in TFS Build with DPAPI

I’ve blogged some days ago on Securing the password in build definition. I want to make a disclaimer on this subject. The technique described in that article permits you to use encrypted password in a build definition, but this password cannot be decrypted only if you have no access to the build machine. If you are a malicious user and you can schedule a build, you can simply schedule a new build that launch a custom script that decrypts the password and sends clear password by email or dump to the build output.

The previous technique is based on encrypting with DPAPI, encrypted password can be decrypted only by TfsBuild user and only in the machine used to generate the password (build machine). Despite the technique you used to encrypt the password, the build process should be able to decrypt the password, so it is possible for another user to schedule another build running a script that decrypt the password.

Every user that knows the TfsBuild user password can also remote desktop to build machine, or using Powershell Remoting to decrypt the password from the build server. This means: the technique described is not 100% secure and you should be aware of limitation.

Apart from these discussions on the real security of this technique, one of the drawbacks of using DPAPI is you need to do some PowerShell scripting in the remote machine to encrypt the password. So you need to remote Desktop build machine or you need to do a remote session with PowerShell. A better solution is creating a super simple asp.net Site that will encrypt the password with a simple HTML page, then deploy that site on the Build Server.

The purpose is having a simple page running on build server with credentials of TfsBuild that simply encrypt a password using  DPAPI

image

Figure 1: Simple page to encrypt a string.

You can test locally this technique simply running the site in localhost using the same credentials of logged user, encrypting a password and then try to decrypt in powershell.

image

Figure 2: Decrypting a password encrypted with the helper site should work correctly.

The code of this page is really stupid, here is the controller.

[HttpPost]
public ActionResult Index(String pwd)
{
    var pbytes = Protect(Encoding.Unicode.GetBytes(pwd));
    ViewBag.Encrypted = BitConverter.ToString(pbytes).Replace("-", "");
    return View();
}

public static byte[] Protect(byte[] data)
{
    try
    {
        // Encrypt the data using DataProtectionScope.CurrentUser. The result can be decrypted 
        //  only by the same current user. 
        return ProtectedData.Protect(data, null, DataProtectionScope.CurrentUser);
    }
    catch (CryptographicException e)
    {
        Console.WriteLine("Data was not encrypted. An error occurred.");
        Console.WriteLine(e.ToString());
        return null;
    }
}

And the related view.

@{
    ViewBag.Title = "Index";
}

<h2>Simple Powershell Encryptor utils</h2>
<form method="post">


    Insert your string <input type="password" name="pwd" />
    <br />
    <input type="submit" value="Encrypt" />
    <br />
    <textarea cols="80
              " rows="10" >@ViewBag.Encrypted</textarea>

</form>

Thanks to this simple site encrypting the password is much more simpler than directly using powershell and you do not need to remote desktop to build machine. To have a slightly better security you can disable remote desktop and remote powershell in the Build Machine so noone will be able to directly use PowerShell to decrypt the password, even if they know the password of TfsBuild user.

Related Articles

Gian Maria.

Store secure password in TFS Build Definition

Some days ago I had some tweet exchange with Giulio about a post of Gordon on storing security info in TFS Build Definition. The question is: how can I store password in build definition without people being able to view them simply editing the build definition itself?

With TFS 2013 a nice new Build template that allow customization with scripts is included and this is my preferred build customization scenario. Now I question myself on How can I pass a password to a script in build definition in a secure way? When you are on Active Directory, the best solution is using AD authentication. My build server runs with credentials of user cyberpunk\\TfsBuild where cyberpunk is the name of my domain and the build is executed with that credentials. Any software that supports AD authentication can then give rights to TfsBuild users and there is no need to specify password in build definition.

As an example, if you want to use Web Deploy to deploy a site in a build you can avoid storing password in Clear Text simply using AD authentication. I’ve described this scenario in the post how to Deploy from a TFS Build to Web Site without specifying password in the build definition.

But sometimes you have services or tools that does not supports AD authentication. This is my scenario: I need to call some external service that needs username and password in querystring; credentials are validated against custom database. In this scenario AD authentication could not be used. I’ve setup a simple web service that ask for username and password, and returns a json that simply dumps parameters. This simple web service will represent my external service that needs to be invoked from a script during the build.

image

Figure 1: Simple service that needs username and password without supporting AD Authentication.

As you can see the call does nothings except returning username and password to verify if the script was really called with the rights parameters. Here is a simple script that calls this service, this script can be invoked during a TFS Build with easy and it is my preferred way to customize TFS 2013 build.

Param
(
[string] $url = "http://localhost:2098/MyService/Index",
[string] $username = "",
[string] $password = ""
)

Write-Host "Invoking-Service"

$retValue = Invoke-RestMethod $url"?username=$username&password=$password"  

Write-Host "ReturnValueIs: "$retValue.Message

Once I’ve cheked-in this script in source code, invoking it in TFS Build is a breeze, here is how I configured the build to invoke the service after source code is built.

image

Figure 2: Invoke script but password is in clear text.

This works perfectly, you can verify in the build Diagnostics that the web site was correctly called with the right username and password (Figure 3), but as you can see in Figure 2 password is in clear text, everyone that has access to the build now knows the password. This is something that could no be accepted in some organization, so I need to find a way to not specify password in clear text.

image

Figure 3: Web site was called with the right password.

My problem is: how can I pass a password to the script in a secure way?

Luckily enough, windows implements a set of secure API called DPAPI that allows you to encrypt and decrypt a password using user/machine specific data. This means that a string encrypted by a user on a machine can be decrypted only by that user on the same machine and not from other users.

Thanks to DPAPI we can encrypt the password using Cyberpunk\\TfsBuild user from build machine, then use encrypted password in build definition.

Anyone that looks at build definition will see the encrypted password, but he could not decrypt unless he knows credentials of Cyberpunk\\TfsBuild user and runs the script on the same Build machine.

Build agent can decrypt the password because it runs as Cyberpunk\\TfsBuild user on the Build machine.

Now I remote desktop on the Build Machine, opened a powershell console using credentials of Cyberpunk\TfsBuild user, then I encrypted the password with the following code. For this second example the password will be MyPassword to distinguish from previous example.

PS C:\Users\Administrator.CYBERPUNK> $secureString = ConvertTo-SecureString -String "MyPassword" -AsPlainText -Force
PS C:\Users\Administrator.CYBERPUNK> $encryptedSecureString = ConvertFrom-SecureString -SecureString $secureString
PS C:\Users\Administrator.CYBERPUNK> Write-Host $encryptedSecureString
01000000d08c9ddf0115d1118c7a00c04fc297eb010000007b3f6d7796acef42b98128ebced174280000000002000000000003660000c00000001000
0000dee3359600e9bfb9649e94f3cfe7b24f0000000004800000a000000010000000e12de6a220f9a542655d75356be128511800000012a173b8fe8b
09244f7050da6784289a308ce6888ace493614000000e3dcb31c16ac3ff994d50dac600ed766d746e901

Encrypted password is that long string you see in the above script and can be used in build definition instead of a clear-text password.

image

Figure 4: Password is now encrypted in the build definition

This password can be decrypted only by users that knows the password of TfsBuild user and can open a session in the Build machine. The main drawback of this technique is that the person that creates the build (and knows the password for the external service) should know also the password of TfsBuild user and access to Build machine to encrypt it. This problem will be fixed in a future post, for now I’m happy enough of not having clear text password in build definition.

Clearly the script that invokes the service should be modified to takes encryption into account:

Param
(
[string] $url = "http://localhost:2098/MyService/Index",
[string] $username = "",
[string] $password = ""
)

Write-Host $password
$secureStringRecreated = ConvertTo-SecureString -String $password

$cred = New-Object System.Management.Automation.PSCredential('UserName', $secureStringRecreated)
$plainText = $cred.GetNetworkCredential().Password

Write-Host "Invoking-Service"

$retValue = Invoke-RestMethod $url"?username=$username&password=$plainText"  

Write-Host "ReturnValueIs: "$retValue.Message

This code simply decrypts the password and then calls the service. This is a simple piece of powershell code I’ve found on some sites, nothing complex. Then I checked in this new script and fire the build. After the build completes I verified that the script correctly decrypted the right password and that the service was invoked with the right decrypted password.

image

Figure 5: Script correctly decrypted the password using TFSBuild credentials

To verify that this technique is secure I connected as Domain Administrator, edited the build and grabbed encrypted password from the definition. Once I’ve got the encrypted password I run the same PowerShell script to decrypt it, but I got an error.

image

Figure 6: I’m not able to decrypt the string once encrypted by a different user

Even if I’m a Domain Admin, I could not decrypt the password, because I’m a different user. It is not a matter of permission or of being or not ad administrator, the original password is encrypted with data that is available only for the same combination of user/machine, so it is secure.

If you have multiple build controllers / agent machines, you can still use this technique, but you need to specify the build machine you used to generate the password in the build definition.

image

Figure 7: I specified the exact agent that should run the build, because it is on the machine where I’ve encrypted the password.

In this example I’ve used powershell, but the very same technique can be used in a Custom Action because DPAPI is available even in C#.

Gian Maria.

Publish NuGet Package to a private NuGet Server with TFS Build and Symbol Server

Previous post on the series

After you set automatic publishing of NuGet packages with automatic assembly and NuGet version numbering in a TFS Build, you surely want to enable publishing symbols on a Symbol Server. This will permits you to put a reference to your NuGet Package and then being able to debug the code thanks to Symbol Server support with TFS. Publishing symbols is just a matter of specifying a shared folder to store symbols in build configuration, but if you enable it in previous build where you publish with Powershell, it does not work. The reason is, you are running PowerShell script that publish NuGet package after build (or after test), but in the build Workflow, source indexing happens after these steps.

 

image

Figure 1: Build workflow sequence showing that publish of symbols takes place after NuGet package is published

The problem happens because you should publish your NuGet package after publishing took place and your .pdb files are modified to point to sources in TFS. To fix this problem you simply need first to download the Build Workflow, (in TFS 2013 default build Workflows are not stored directly in Source Control and they should be downloaded if you want to customize them) and create a custom build.

image

Figure 2: Downloading the standard Workflow Template in your machine to create a custom Workflow

You can just download to your computer, change name of the file, change inner workflow and check-in in Source Control as you would have done with previous version of TFS. My goal is adding the ability to run another script at the very end of the workflow, so I opened the workflow and simply copy and paste the Run optional script after Test in the end of the sequence and change name to Run optional script at the end of the Build.

image

figure 3: Copy and paste script execution block to enable executing script at the end of the Workflow

Now I added two other Workflow Arguments , to allow the user to specify the location and arguments of this script.

image

Figure 4: Adding arguments to pass to the new script execution block.

Now you should change the Run optional script at the end of the Build block to reference these two new arguments instead of the original ones.

image

Figure 5: Referencing the new arguments in copied block

Finally you need to return to Arguments of the Workflow and change the Metadata argument, to specify some additional data about these two arguments.

image

Figure 6: Adding Metadata for your custom arguments.

Here you can give name and description to your arguments, but the most important part is giving a category and the Editor. In this example the PostExecutionScriptPath should be a Source Control path like $/TeamProjectName/xxx and if you specify

Microsoft.TeamFoundation.Build.Controls.ServerFileBrowserEditor, Microsoft.TeamFoundation.Build.Controls

As Editor, the user would be able to browse the source control as for the other script in the build. You should check-in the new workflow in source control, edit the build definition and in Process tab choose the new workflow. You should be able now to see the new arguments to specify the script that should be run at the end of the Build.

image

Figure 7: You can now specify a script that will be executed at the very end of the build.

Thanks to the Editor, if you select the Post Execution Script Path, you will find an ellipsis button that permits you to browse the source to specify file location.

image

Figure 8: Thanks to the Editor property in metadata you are able to browse the source to specify the script.

You can now use the very same script of NuGet publishing, but since it is executed after symbols publishing, now your NuGet package contains indexed pdb and everything works as expected.

Gian Maria.

Automatically build and publish nuget packages during TFS Build

Previous post on the series

Using powershell to cusotmize build is simple and easy, once you have versioning in place (previous article), if you are realizing some form of reusable library it is time to think on how to distribute it to people. One of the obvious choice is using Nuget. Luckily enough, setting up a nuget server in an azure website is just a matter of

  1. Create an empty asp.net MVC project
  2. Reference standard Nuget Server package
  3. Change web config and add a password for publishing
  4. Publish on an azure web sites.

Et voila, you have your Nuget server up and running in really no-time on an azure web site, it is simple and quick to setup.

image

Figure 1: Nuget server running on Windows Azure Web Site

Now you only need to create a simple powershell file to enable automatic publishing of your libraries during TFS Build. This is really easy, first of all I started including a standard nuspec file inside the library I want to publish. This .nuspec file will contains all the details of the package and the script will only need to change the number, create nuget package and finally publish the package into your nuget server.

Here is a simple .nuspect file for a test project of a log library.

<?xml version="1.0"?>
<package >
  <metadata>
    <id>LogLibrary</id>
    <version>1.0.0.0</version>
    <authors>Ricci Gian Maria</authors>
    <owners>Ricci Gian Maria</owners>
    <requireLicenseAcceptance>false</requireLicenseAcceptance>
    <description>Simple log library project to verify build+nuget.</description>
    <releaseNotes>ContinuousIntegration.</releaseNotes>
    <copyright>Copyright 2014 - Ricci Gian Maria</copyright>
    <tags>Example</tags>
  </metadata>

  <files>
    <file src="LogLibrary.dll" target="lib\NET40" />
    <file src="LogLibrary.pdb" target="lib\NET40" />
  </files>
</package>

As you can verify it is a standard nuspec file and my goal is creating a powershell script that

  1. Change version including build number and day in format YYDDD where DDD is the number of day of the year
  2. Execute nuget to create pack file and publish to my server

The very first step is including nuget.exe inside my BuildScript folder and check-in everything. This will assure me that everything is needed for the build is contained in source control.

image

Figure 2: Content of the BuildScript folder

Now the interesting part, the powershell function that does the real work.

function Publish-NugetPackage
{
  Param 
  (
    [string]$SrcPath,
    [string]$NugetPath,
    [string]$PackageVersion, 
    [string]$NugetServer,
    [string]$NugetServerPassword
  )
    
    $buildNumber = $env:TF_BUILD_BUILDNUMBER
    if ($buildNumber -eq $null)
    {
        $buildIncrementalNumber = 0
    }
    else
    {
        $splitted = $buildNumber.Split('.')
        $buildIncrementalNumber = $splitted[$splitted.Length - 1]
    }
    
    Write-Host "Executing Publish-NugetPackage in path $SrcPath, PackageVersion is $PackageVersion"
    
    $jdate = Get-JulianDate
    $PackageVersion = $PackageVersion.Replace("J", $jdate).Replace("B", $buildIncrementalNumber)
    
    Write-Host "Transformed PackageVersion is $PackageVersion "
 
    $AllNuspecFiles = Get-ChildItem $SrcPath\*.nuspec
  
    #Remove all previous packed packages in the directory
     
    $AllNugetPackageFiles = Get-ChildItem $SrcPath\*.nupkg
  
    foreach ($file in $AllNugetPackageFiles)
    { 
        Remove-Item $file
    }

    foreach ($file in $AllNuspecFiles)
    { 
        Write-Host "Modifying file " + $file.FullName
        #save the file for restore
        $backFile = $file.FullName + "._ORI"
        $tempFile = $file.FullName + ".tmp"
        Copy-Item $file.FullName $backFile -Force
        #now load all content of the original file and rewrite modified to the same file
        Get-Content $file.FullName |
        %{$_ -replace '<version>[0-9]+(\.([0-9]+|\*)){1,3}</version>', "<version>$PackageVersion</version>" } > $tempFile
        Move-Item $tempFile $file.FullName -force

        #Create the .nupkg from the nuspec file
        $ps = new-object System.Diagnostics.Process
        $ps.StartInfo.Filename = "$NugetPath\nuget.exe"
        $ps.StartInfo.Arguments = "pack `"$file`""
        $ps.StartInfo.WorkingDirectory = $file.Directory.FullName
        $ps.StartInfo.RedirectStandardOutput = $True
        $ps.StartInfo.RedirectStandardError = $True
        $ps.StartInfo.UseShellExecute = $false
        $ps.start()
        if(!$ps.WaitForExit(30000)) 
        {
            $ps.Kill()
        }
        [string] $Out = $ps.StandardOutput.ReadToEnd();
        [string] $ErrOut = $ps.StandardError.ReadToEnd();
        Write-Host "Nuget pack Output of commandline " + $ps.StartInfo.Filename + " " + $ps.StartInfo.Arguments
        Write-Host $Out
        if ($ErrOut -ne "") 
        {
            Write-Error "Nuget pack Errors"
            Write-Error $ErrOut
        }
        #Restore original file
        #Move-Item $backFile $file -Force
    }
    
    $AllNugetPackageFiles = Get-ChildItem $SrcPath\*.nupkg
  
    foreach ($file in $AllNugetPackageFiles)
    { 
        #Create the .nupkg from the nuspec file
        $ps = new-object System.Diagnostics.Process
        $ps.StartInfo.Filename = "$NugetPath\nuget.exe"
        $ps.StartInfo.Arguments = "push `"$file`" -s $NugetServer $NugetServerPassword"
        $ps.StartInfo.WorkingDirectory = $file.Directory.FullName
        $ps.StartInfo.RedirectStandardOutput = $True
        $ps.StartInfo.RedirectStandardError = $True
        $ps.StartInfo.UseShellExecute = $false
        $ps.start()
        if(!$ps.WaitForExit(30000)) 
        {
            $ps.Kill()
        }
        [string] $Out = $ps.StandardOutput.ReadToEnd();
        [string] $ErrOut = $ps.StandardError.ReadToEnd();
        Write-Host "Nuget push Output of commandline " + $ps.StartInfo.Filename + " " + $ps.StartInfo.Arguments
        Write-Host $Out
        if ($ErrOut -ne "") 
        {
            Write-Error "Nuget push Errors"
            Write-Error $ErrOut
        }

    }
}

 

My standard disclaimer is: I’m not a powershell expert, so I think that probably there are gazillions of way to do a better powershell script. The function is really simple, first of all it starts doing some standard transformation of the package number (as in previous article), then it starts enumerating all .nuspec files present in bin directory of the build. For each nuspec file, it simply do a backup, then change the version number using a simple regex, finally it invoke the nuget.exe process to create the pack file. To launch an external process and have full control on the output I decided to use (since I’m a .NET programmer) the System.Diagnostic.Process class, that permits me to intercept all standard output and standard error.

After the process ends I use write-host and write-error to dump all the output to the host (it would be a better approach parsing the output and generate a better error message). Finally I invoke again nuget.exe to publish the package to my server. Once this function is in place, you can invoke with a simple script that will be scheduled to be executed AfterBuild.

Param
(
[string] $PackageVersion = "1.0.J.B",
[string] $NugetServer = "http://alkampfernuget.azurewebsites.net/",
[string] $NugetServerPassword = "This_is_my_password"
)

Write-Host "Running Pre Build Scripts"

$scriptRoot = Split-Path -Parent -Path $MyInvocation.MyCommand.Definition
Import-Module $scriptRoot\BuildFunctions

Publish-NugetPackage $scriptRoot\..\..\bin $scriptRoot $PackageVersion $NugetServer $NugetServerPassword

It is based mainly by convention, the script root is the folder where the script is, and the build is configured with this workspace.

image

Figure 3: Workspace configuration

The above relative path maps all ScriptTest folder inside the $(SourceDir) of the agent, the script is in the BuildTools directory, so I need to get the parent folder to locate the root of the workspace and another \.. to find the build base directory that contains the bin folder, where the build copy all the output. An even better situation would be using TF_BUILD_BINARIESDIRECTORY environment variable, that automatically locates that folder, but that variable is populated only during a build. If you plan to use the script only inside a TFS Build, you can safely use TF_BUILD_BINARIESDIRECTORY variable. Here is a modified example that does this

Param
(
[string] $PackageVersion = "1.0.J.B",
[string] $NugetServer = "http://alkampfernuget.azurewebsites.net/",
[string] $NugetServerPassword = "This_is_my_password"
)

Write-Host "Running Pre Build Scripts"
$scriptRoot = Split-Path -Parent -Path $MyInvocation.MyCommand.Definition

#Remove-Module BuildFunctions
Import-Module $scriptRoot\BuildFunctions

$binPath = $env:TF_BUILD_BINARIESDIRECTORY
if ($binPath -eq $null)
{
	Write-Host "Not running in build, using relative path to identify bin location."
    $binPath = $scriptRoot + "\..\..\bin"
}

Publish-NugetPackage $env:TF_BUILD_BINARIESDIRECTORY $scriptRoot $PackageVersion $NugetServer $NugetServerPassword

The above script does nothing except importing the module with the publishing function and invoke the Publish-NugetPackage function.

image 

Figure 4: Invoke the script after build

Now everything is setup correctly, just fire a build and verify that everything is done correctly looking at write-host messages that gets collected in the detailed log of the build. Thanks to log files, you can output as much information you want to diagnose problem that can arise during the build.

image

figure 5: Verify output of powershell scriptin diagnostic build informations.

If the script contains some errors, powershell will write error with write-error and this information will make the build partially fails and also all the errors will be output in build details, but not in a real nice form. Since I’ve intercepted all nuget.exe error output and dump with Write-Error, a nuget.exe error message will make the build Partially Fails and you can looks at errors list to understand what happened.

image

Figure 6: How errors in powershell are displayed in build output

This has not a nice formatting, because each error line is treated as a single and distinct error in the build. But at least we are able to identify the root cause of the error, even if they are not really well formatted. When the build is green you will find your packages in the feed of your nuget server.

image

Figure 7: Feed of my packages pushed during the build.

Each good build will produce a unique version for your package, as you can verify from package console.

image

Figure 8: Listing available packages in Package Manager Console.

With few lines of powershell you are able to automatically publish your packages to nuget server during TFS Build.

Gian Maria.

Versioning assembly during TFS 2013 build with Powershell Scripts

One of the most important news in TFS Build 2010 is the introduction of Workflow Foundation that replaced standard MSBuild scripts used in TFS 2008. Workflow foundation can be really powerful, but indeed it is somewhat scaring and quite often customizing a build can be complex.

You can find some blog post of mine on the subject:

Years are passed, but I still see people scared when it is time to customize the build, especially because the Workflow can be a little bit intimidating. In TFS2013 the build is still managed by Workflow Foundation, but the new workflow basic template now supports simply customization with scripts.

image

Figure 1: Pre-build script configured in the build.

If you look at previous image I’ve setup a simple script to manage assembly versioning and I’ve configured it to run Before the Build, I specified version number thanks to script arguments: -assemblyVersion 1.2.0.0 -fileAssemblyVersion 1.2.J.B. The notation J and B is taken from a nice tool called Tfs Versioning; it is used to manage versioning of assembly with a Custom Action. If you ever used that tool since the beginning (Tfs 2010) you probably discovered that managing build customization with Custom Action is not so easy. Setting up Tfs Versioning tools is super easy if you simply use workflow included in the release, but if you already have a customized workflow, you need to modify the Workflow adding the Custom Activity in the right place with the right parameters.

When you move to TFS 2012 and then to 2012, you need to download the source of the tool and recompile against latest version of build assemblies, then you need to modify the workflow again. This is one of the most annoying problem of Custom Actions, you need to recompile again when you upgrade build servers to new version of TFS.

A script solution is surely more reusable, so I’ve decided to create a little script in Powershell to modify all assemblyinfo.cs files before the build. I’m not a Powershell expert and my solution is not surely optimal, but is a simple proof of concept on how simple is to accomplish build versioning with a script instead of direct customization of the workflow with custom actions.

To have reusable code I’ve create a script called BuildFunctions.psm1 that contains all functions that can be useful during the build. Here it is the function that takes care of versioning for C# projects.

function Update-SourceVersion
{
  Param 
  (
    [string]$SrcPath,
    [string]$assemblyVersion, 
    [string]$fileAssemblyVersion
  )
    
    $buildNumber = $env:TF_BUILD_BUILDNUMBER
    if ($buildNumber -eq $null)
    {
        $buildIncrementalNumber = 0
    }
    else
    {
        $splitted = $buildNumber.Split('.')
        $buildIncrementalNumber = $splitted[$splitted.Length - 1]
    }

    if ($fileAssemblyVersion -eq "")
    {
        $fileAssemblyVersion = $assemblyVersion
    }
    
    Write-Host "Executing Update-SourceVersion in path $SrcPath, Version is $assemblyVersion and File Version is $fileAssemblyVersion"
 

    $AllVersionFiles = Get-ChildItem $SrcPath AssemblyInfo.cs -recurse

    
    $jdate = Get-JulianDate
    $assemblyVersion = $assemblyVersion.Replace("J", $jdate).Replace("B", $buildIncrementalNumber)
    $fileAssemblyVersion = $fileAssemblyVersion.Replace("J", $jdate).Replace("B", $buildIncrementalNumber)
    
    Write-Host "Transformed Version is $assemblyVersion and Transformed File Version is $fileAssemblyVersion"
 

    foreach ($file in $AllVersionFiles) 
    { 
        Write-Host "Modifying file " + $file.FullName
        #save the file for restore
        $backFile = $file.FullName + "._ORI"
        $tempFile = $file.FullName + ".tmp"
        Copy-Item $file.FullName $backFile
        #now load all content of the original file and rewrite modified to the same file
        Get-Content $file.FullName |
        %{$_ -replace 'AssemblyVersion\("[0-9]+(\.([0-9]+|\*)){1,3}"\)', "AssemblyVersion(""$assemblyVersion"")" } |
        %{$_ -replace 'AssemblyFileVersion\("[0-9]+(\.([0-9]+|\*)){1,3}"\)', "AssemblyFileVersion(""$fileAssemblyVersion"")" }  > $tempFile
        Move-Item $tempFile $file.FullName -force
    }
 
}

If you are a powershell expert, please do not shoot the pianist :), this is my very first serious Powershell script. The cool part is that Build Subsystem stores some interesting values in Environment Variables, so I can simply found the actual build number with the code: $buildNumber = $env:TF_BUILD_BUILDNUMBER. The rest of the script is simply string and Date manipulation and a RegularExpression to replace AssemblyVersion and AssemblyFileVersion in original version of the file, you can find details Here in the original post I’ve used as a sample for my version.

The cool part about this script is that you can import it in another script to use functions decalred in it. The goal is maintaining all complexities inside this base script and use these functions in real scripts that gets called by TFS Build. This simplify maintenance, because you only need to maintain function only once.  As an example this is the simple PreBuild script that manages assembly numbering.

 Param
(
[string] $assemblyVersion,
[string] $fileAssemblyVersion
)


Write-Host "Running Pre Build Scripts"

$scriptRoot = Split-Path -Parent -Path $MyInvocation.MyCommand.Definition

Import-Module $scriptRoot\BuildFunctions

if ($assemblyVersion -eq "")
{
    $assemblyVersion = "2.3.0.0"
    $fileAssemblyVersion = "2.3.J.B"
}
 

Thanks to Import-Module commandlet I can import all functions of file BuildFunctions.psm1. This is much more simpler than having Custom Actions and insert them inside the workflow.

One of the coolest part of Powershell scripts is the ability to define parameters in the head of the script so you can specify them with notation –parameter value leaving all the parsing burden of commandline to Powershell infrastructure. Another cool part of this approach is that you can include a base AssemblyVersion number to be used if no valid number is passed by Command Line. Thanks to Git Support you can create a simple build definition valid for multiple branches and in such scenario it is much better to have version number stored in source control, because you can specify a different build number for each branch.

Now just fire a build and verify that assemblies in drop folder contains correct numbering.

image

Figure 2: Verify that all the assemblies contains the correct AssemblyVersionFile Number

If something went wrong, you can look at the diagnostic of the build to verify what is happened in the script. All Write-Host directive are in fact intercepted and are collected inside the diagnostics of the build.

image

Figure 3: Output of the scripts is collected inside the Diagnostic of the build.

Thanks to this approach you can

  • Create a simple reusable set of functions inside a Powershell Base Script
  • Use functions contained in Powershell Base Script inside simpler script for each build
  • All Write-Host output is redirected in diagnostic log of the build to diagnose problem.
  • Customization is still valid if you upgrade TFS
  • You can customize the build simply specifying path of the script.
  • You can test the script running it locally

If you compare how simple is this approach compared to managing customization of Workflow with Custom Action you can understand what a great improvement you got using the new TFS Build templates.

Gian Maria.