Thursday, 30 November 2017

TFS 2017 Build System - Maintain last "N' builds

In my last blog post, I described retention policies in the TFS 2017 build system. I described how different it is from the the retention policies we get in XAML build system. 

One of the limitations I found in the new style retention policy is that I couldn't retain a specific number of builds for each status. We needed to do it for some builds that are triggered very frequently (once every couple of minutes) and check if there is some work to be done. If it found work, it would do it, otherwise it will reschedule a build for itself after a couple of minutes. Another scenario, where you might have a lot of builds is when you it is triggered by a commit of a very busy repository.

So, in order for us to retain only "N" builds for each status, we created a Powershell Module to clean up builds. In the module, we create a command-let that takes as parameter the name of the build, the number of builds to keep, the result filter and tag filter. Our command-let looks as following



***************************************************
.SYNOPSIS
 Cleans up all builds for the given build definition keeping the latest N number of builds where n is passed a parameter
 If a status is provided, it would only keep N builds with the given status

.DESCRIPTION
 PATCH https://{instance}/DefaultCollection/{project}/_apis/build/builds/{buildId}?api-version={version}
 Uses api-version 2.0 to update the build result
***************************************************
function Cleanup-Builds([string] $tfsCollection,
                    [string] $tfsProject,
                    [string] $buildDefinitionName,
                    [int] $numberOfBuildsToKeep = 10,
                    [string] $result="",
                    [string] $tagsFilter = "")
{
    if (${env:system.debug} -eq $true) {
        $VerbosePreference="Continue"
    }

    if ($status -eq ""){
        Write-Verbose "Deleting all but the latest $numberOfBuildsToKeep builds for definition $buildDefinitionName."
    }
    else{
        Write-Verbose "Deleting all but the latest $numberOfBuildsToKeep builds for definition $buildDefinitionName with status $status."
    }

    $buildDefinition = Find-BuildDefinition($buildDefinitionName)
    if ($buildDefinition -eq $null) {
        Write-Error "No build definition found $buildDefinitionName"
        return
    }

    $buildDefinitionId = $buildDefinition.id
    $query = [uri]::EscapeUriString("$tfsCollection$tfsProject/_apis/build/builds?api-version=2.0&definitions=$buildDefinitionId&queryOrder=2&resultFilter=$result&tagFilters=$tagsFilter&`$top=5000")

    $builds = Invoke-RestMethod -Method GET -UseDefaultCredentials -ContentType "application/json" -Uri $query
    $retainedBuild = 0
    $deletedBuildCount = 0
    for ($i = $builds.Count - 1; $i -gt -1; $i--) {
        $build = $builds.value[$i]
        $buildId = $build.id
        $buildNumber = $build.buildNumber
        
        try {
            $query = [uri]::EscapeUriString("$tfsCollection$tfsProject/_apis/build/builds/$buildId/tags?api-version=2.0")
            $tagFound = $false

            # Not delete the latest numberOfBuildsToKeep builds
            if ( ($retainedBuild -lt $numberOfBuildsToKeep)) {
                $retainedBuild = $retainedBuild + 1
            }
            else {
                Write-Verbose "Deleting build $buildNumber"
                $query = [uri]::EscapeUriString("$tfsCollection$tfsProject/_apis/build/builds/$buildId`?api-version=2.0")
                Invoke-RestMethod -Method DELETE -UseDefaultCredentials -ContentType "application/json" -Uri $query
                $deletedBuildCount = $deletedBuildCount + 1
            }
        }
        catch {
            Write-Error "StatusCode:" + $_.Exception.Response.StatusCode.value__ +
                        "`r`nStatusDescription:" + $_.Exception.Response.StatusDescription
        }
    }
        
    Write-Output "Deleted $deletedBuildCount builds for build definition $buildDefinitionName"
}

We create a PowerShell Module file for the above command let. To set up the Powershell module, we modified the PSModulePath environment variable as first step of our build to include the module path. Then to set-it all up we added a PowerShell task group calling the Cleanup-Builds command in an inline script as shown below




Our build definition looks like below





No comments: