Wednesday, July 29, 2015

Build and publish to NuGet as part of TFS build process

NuGet is a free, open source package management system for .NET platform, which helps in easy dependency management. Using NuGet very easy for project owners to build and define package dependencies which will be included in the package during the packaging process and later publish this to the repository for usage by external users.

Users can later install the package by searching in the NuGet source repository which will automatically download and install the dependencies as well. You can use the NuGet command line utility to create packages from a specification file and later publish them to the repository whenever needed. If you want to automate the process of package creation and publishing from a TFS build process, you can make use of custom PowerShell scripts that can be invoked as part of the build process. The Team Foundation Build template (TfvcTemplate.12.xaml) provides the capability to extend the build process by using PowerShell scripts at stages like Pre build and Post build.

You can use the default template to run PowerShell and batch (.bat) scripts before and after you compile your code, and before and after your run your tests. During the script execution you can make use of the Team foundation build environment variables to get key bits of data that you need for your build process logic.

I've created a PowerShell module that contains the functions that I need to build and publish a nuget package.

$ErrorActionPreference = "Stop"

Function Publish-NugetPackage
{
    param
    (
        [Parameter(Mandatory = $true, Position = 0)]
        [ValidateNotNullOrEmpty()]
        [string] $Project
    )
    # Read the product version from the assembly
    $version = Get-ChildItem -Filter "$($Project).dll" `
                             -Recurse `
                             -Path (Join-Path $env:TF_BUILD_BUILDDIRECTORY bin) `
                             -File | select -First 1 | select -expand VersionInfo | select -expand ProductVersion

    # Read the contents of the nuspec file
    $nuspecFile = Get-ChildItem -Filter "$($Project).nuspec" `
                                -Recurse `
                                -Path $env:TF_BUILD_SOURCESDIRECTORY -File | select -First 1

    $nuspecContents = Get-Content $nuspecFile.FullName

    # Replace the version of the nuspec file with the assembly version
    Set-Content -Path $nuspecFile.FullName `
                -Value ($nuspecContents -replace "[0-9]+(\.([0-9]+|\*)){1,3}", "$($version)
") `
                -Force
    # Create the nuget package
    New-NugetPackage $nuspecFile.FullName

    # Fetch the newly created nuget package
    $nugetPackage = Get-ChildItem -Filter "*.nupkg" `
                                  -Recurse `
                                  -Path $env:TF_BUILD_SOURCESDIRECTORY -File | select -First 1

    # Publish the nuget package to nuget repository
    Push-NugetPackage $nugetPackage.FullName

}

Function New-NugetPackage
{
    param
    (
        [Parameter(Mandatory = $true, Position = 0)]
        [ValidateNotNull()]
        [ValidateScript({Test-Path $_})]
        [String] $NuspecFile,

        [Parameter(Position= 1)]
        [ValidateScript({Test-Path $_ -PathType Leaf -Include "nuget.exe"})]
        [String] $NugetLocation = (Join-Path $PSScriptRoot "nuget.exe")
    )

    $command = $NugetLocation + " pack $($NuspecFile)"

    Invoke-Expression $command
}

Function Push-NugetPackage
{
     param
    (

        [Parameter(Position= 3)]
        [string] $Source = "http://prajeeshnuget.azurewebsites.net/nuget",

        [Parameter(Mandatory = $true, Position = 0)]
        [ValidateNotNull()]
        [ValidateScript({Test-Path $_})]
        [string] $PackagePath,
       
        [Parameter(Position= 2)]
        [string] $APIKey = "MY_NUGET_APIKEY",

        [Parameter(Position= 1)]
        [ValidateScript({Test-Path $_ -PathType Leaf -Include "nuget.exe"})]
        [String] $NugetLocation = (Join-Path $PSScriptRoot "nuget.exe")
    )

    $command = $NugetLocation + " push $($PackagePath) $($APIKey)"
    if(-not ([String]::IsNullOrEmpty($Source)))
    {
        $command += " -s "  + $Source
    }

    Invoke-Expression $command | Out-Null
}

Export-ModuleMember -Function Publish*

The module contains logic to update the NuSpec file with the latest product version from the assembly that is build. Once updated this file will be used to create a nuget package and then published to the repository. As you can see, I’ve used the TF_BUILD_SOURCESDIRECTORY and TF_BUILD_BUILDDIRECTORY to get the source location and build location respectively in the module. To read more about TF_BUILD variables, refer to the link https://msdn.microsoft.com/en-us/library/hh850448.aspx.

I’ve created a script that I use to invoke the functions in this module

param
(
       [string]$Project
)

$exitCode = 0
trap
{
    $Host.UI.WriteErrorLine($error[0].Exception.Message)
    $Host.UI.WriteErrorLine($error[0].Exception.StackTrace)
    if ($exitCode -eq 0)
    {
        $exitCode = 1
    }
}


Import-Module .\TFSNugetExtensions.psm1
Publish-NugetPackage $Project
exit $exitCode

The next step is to publish these scripts along with the Nuget.exe file to the source control, so that I can refer these files as part of my build definition. I’ve added these files into the BuildExtensions folder in my team project

Now we can go ahead and create the build definition for our project. While creating the build definition it’s important to create the source settings mapping for the scripts folder so that they are also available as part of the build process.


In the process tab, you can now call the script which we have created as part of the post build execution phase.



That’s it, next time you queue a build, you can see that the build creates and uploads a nuget package to the repository automatically.

No comments: