scripts/InvokePipeline.ps1 (570 lines of code) (raw):

param ( [Parameter(Mandatory=$false)] [ValidateNotNullOrEmpty()] [System.String] $PipelineName, [Parameter(Mandatory=$false)] [ValidateNotNullOrEmpty()] [System.String] $DisplayName, [Parameter(Mandatory=$false)] [ValidateNotNullOrEmpty()] [System.String] $Owner, [Parameter(Mandatory=$false)] [Int] $PipelineDefinitionId = 21, [Parameter(Mandatory=$false)] [ValidateNotNullOrEmpty()] [System.String] $SourceBranch, [Parameter(Mandatory=$false)] [System.String] $PipelineParameters, [Parameter(Mandatory=$false)] [System.String] $PipelineType, [Parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] [System.String] $StorageAccountName, [Parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] [System.String] $StorageAccountKey, [Parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] [System.String] $FunctionsVersion, [Parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] [System.String] $DevOpsUserName, [Parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] [System.String] $DevOpsUserPAT, [Parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] [System.String] $OrganizationName, [Parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] [System.String] $ProjectName ) # Assumption: Parameters are grouped by key value pairs. These should be defined as follow: # "key1=value1;key2=value2;key3=value3;" # function ParsePipelineParameters { param ( [Parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] [System.String] $PipelineParameters ) $result = @{} $keyValuePairs = $PipelineParameters -split ";" foreach ($keyValuePair in $keyValuePairs) { $keyValuePair = $keyValuePair.Trim() if ($keyValuePair) { $parts = @($keyValuePair -split "=") if ($parts.Count -ne 2) { WriteLog "Invalid key value pair: $keyValuePair" -Throw } $keyName = $parts[0].Trim() $value = $parts[1].Trim() # Boolean assignment value if ($value -eq "true" -or $value -eq "false") { $value = [System.Convert]::ToBoolean($value) } $result[$keyName] = $value } } return $result } function WriteLog { param ( [Parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] [System.String] $Message, [Switch] $Warning, [Switch] $Throw ) $Message = (GetDatePST).ToString("G") + " -- $Message" if ($Throw) { throw $Message } else { Write-Host $Message } } function GetAuthenticationHeader { $user = $DevOpsUserName $token = $DevOpsUserPAT $base64AuthInfo = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(("{0}:{1}" -f $user, $token))) $authHeader = @{Authorization=("Basic {0}" -f $base64AuthInfo)} return $authHeader } function WaitForPipelineToComplete { Param ( [Parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] $BuildUrl, $WaitTimeInSeconds = 10, $MaxNumberOfTries = 720 ) $authHeader = GetAuthenticationHeader $response = $null $tries = 1 while ($true) { Start-Sleep -Seconds $WaitTimeInSeconds $currentWaitTimeInMinutes = [Math]::Round(($tries*$WaitTimeInSeconds)/60, 2) if (($currentWaitTimeInMinutes) % 1 -eq 0) { WriteLog -Message "Pipeline status: $($response.status)..." WriteLog -Message "Wait time in minutes: $($currentWaitTimeInMinutes)" } $response = Invoke-RestMethod -Method Get -Uri $BuildUrl -Headers $authHeader -MaximumRetryCount 3 -RetryIntervalSec 1 -ErrorAction SilentlyContinue if (-not ($response.status -eq "inProgress" -or $response.status -eq "notStarted")) { WriteLog -Message "Pipeline status: $($response.status)" return $response } if ($tries -ge $MaxNumberOfTries) { WriteLog -Message "Pipeline execution did not complete in $currentWaitTimeInMinutes minutes. See link for current status: $BuildUrl" -Throw } $tries++ } } function GetDevOpsServiceUrl { <# Docs: https://docs.microsoft.com/en-us/rest/api/azure/devops/?view=azure-devops-rest-6.1 instance of the form https://dev.azure.com/{organization}/_apis[/{area}]/{resource}?api-version={version} #> return "https://dev.azure.com/${OrganizationName}/${ProjectName}" } function GetProjectUrl { return "https://${OrganizationName}.visualstudio.com/${ProjectName}" } function InvokeDevOpsPipeline { param ( [Parameter(Mandatory=$true)] [ValidateNotNull()] [PipelineDefinition] $PipelineDefinition, [Parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] [System.String] $StorageAccountName, [Parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] [System.String] $StorageAccountKey, [Parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] [System.String] $FunctionsVersion, [Int] $MaximumNumberOfTries = 5 ) $serviceUrl = GetDevOpsServiceUrl $devOpsUrl = $serviceUrl + "/_apis/build/builds?api-version=6.0" $requestBody = $pipelineDefinition.RequestBody | ConvertTo-Json $projectUrl = GetProjectUrl $buildresponse = $null $buildStatusUrl = $null $pipelineViewUrl = $null $pipelineResult = $null $folderName = $pipelineDefinition.Name.Replace(" ","") $badgesFolderPath = Join-Path $PSScriptRoot $folderName $invocationTime = (GetDatePST).ToString("G") + " (PST)" WriteLog -Message "Queueing pipeline '$($pipelineDefinition.Name)'" $authHeader = GetAuthenticationHeader $buildStatusUrl = $null $buildId = $null $currentCount = 1 $buildStarted = $false if ($pipelineDefinition.Type -eq "Test") { # Integration tests pipelines should be run at most 3 times $MaximumNumberOfTries = 3 } do { try { if (-not $buildStarted) { $buildresponse = Invoke-RestMethod -Method Post ` -ContentType application/json ` -Uri $devOpsUrl ` -Body $requestBody ` -Headers $authHeader ` -MaximumRetryCount 3 ` -RetryIntervalSec 1 ` -ErrorAction Stop $buildStarted = $true } else { # Retry failed jobs WriteLog -Message "Rerunning failed jobs for build '$buildId'. Current retry: $currentCount" $retryJobsUrl = $serviceUrl + "/_apis/build/builds/"+ $buildId + "?retry=true&api-version=6.0" $buildresponse = Invoke-RestMethod -Method Patch ` -ContentType application/json-patch+json ` -Uri $retryJobsUrl ` -Headers $authHeader ` -MaximumRetryCount 3 ` -RetryIntervalSec 1 ` -ErrorAction Stop } $buildStatusUrl = $buildresponse.url WriteLog -Message "Build Status Url: $buildStatusUrl" $pipelineViewUrl = $projectUrl + "/_build/results?buildId=$($buildresponse.Id)&view=results" WriteLog -Message "Pipeline has been queued. Please see '$pipelineViewUrl' for execution status." $script:pipelineInvocationResult["BuildId"] = $buildresponse.Id $buildId = $buildresponse.Id } catch { $message = if (-not $buildStarted) { "Failed to queue pipeline." } else { "Failed to rerun pipeline for build id '$buildId'." } $message += "Exception information: $_" WriteLog -Message $message -Throw } $pipelineResult = WaitForPipelineToComplete -BuildUrl $buildStatusUrl if ($pipelineResult.result -eq "succeeded") { break } # increase the current count $currentCount++ } while ($currentCount -le $MaximumNumberOfTries) # Create last-run.svg file $lastRunFileName = "last-run.svg" $filePath = Join-Path $badgesFolderPath $lastRunFileName $label = if ($PipelineDefinition.Type -eq "Build") { "Last build time" } else { "Last test run time" } NewBadge -Label $label -Content $invocationTime -Color "lightgrey" -FilePath $filePath $script:pipelineInvocationResult["ExecutionTime"] = $invocationTime $summary = "Pipeline $($pipelineDefinition.Type) " $summary += "Result: $($pipelineResult.result) " $summary += "Status: $($pipelineResult.status) " WriteLog -Message $summary # Create pipeline result badge $pipelineResultFileName = "pipeline-result.svg" $filePath = Join-Path $badgesFolderPath $pipelineResultFileName $label = "Build id: $($buildresponse.Id)" $content = $pipelineResult.result $color = if ($pipelineResult.result -eq "succeeded") { "Brightgreen" } else { "red" } $script:pipelineInvocationResult["Status"] = $content NewBadge -Label $label -Content $content -Color $color -FilePath $filePath # Create tests results badge $filePath = Join-Path $badgesFolderPath "test-results.svg" $buildUrl = $projectUrl + "/_apis/test/ResultSummaryByBuild?buildId=$($buildresponse.Id)" NewTestResultBadge -BuildUrl $buildUrl -FilePath $filePath # Save the pipeline information $filePath = Join-Path $badgesFolderPath "pipeline-results.json" Set-Content -Path $filePath -Value $pipelineViewUrl -Force | Out-Null $script:pipelineInvocationResult["BuildUrl"] = $pipelineViewUrl $script:pipelineInvocationResult | ConvertTo-Json -Depth 5 | Set-Content -Path $filePath -Force | Out-Null # Save the BuildUrl and build id in a txt file $filePath = Join-Path $badgesFolderPath "Build-url.txt" Set-Content -Path $filePath -Value $pipelineViewUrl -Force | Out-Null $filePath = Join-Path $badgesFolderPath "Build-id.txt" Set-Content -Path $filePath -Value $buildresponse.Id -Force | Out-Null UploadFilesToStorageAccount -StorageAccountName $storageAccountName -StorageAccountKey $storageAccountKey -SourcePath $badgesFolderPath -FunctionsVersion $FunctionsVersion if ($pipelineResult.result -ne "succeeded") { WriteLog -Message "Pipeline execution was not successful. Pipeline status: $($pipelineResult.result). For more information, please see $($pipelineViewUrl)" -Throw } } <# The URL is of the form https://${OrganizationName}.visualstudio.com/${ProjectName}/_apis/test/ResultSummaryByBuild?buildId=$id, where $id is the build id #> function NewTestResultBadge { param ( [Parameter(Mandatory=$true)] [ValidateNotNull()] [String] $BuildUrl, [Parameter(Mandatory=$true)] [ValidateNotNull()] [String] $FilePath ) WriteLog "Check if '$BuildUrl' has any published tests results" $results = Invoke-RestMethod $BuildUrl -MaximumRetryCount 3 -RetryIntervalSec 1 -ErrorAction Stop if (-not $results.aggregatedResultsAnalysis.resultsByOutcome) { WriteLog "Response does not contain any aggregated results analysis" return } $total = $results.aggregatedResultsAnalysis.totalTests $passed = $results.aggregatedResultsAnalysis.resultsByOutcome.Passed.Count $failed = $results.aggregatedResultsAnalysis.resultsByOutcome.Failed.Count $skipped = $total - $passed - $failed $color = if ($failed -gt 0) { "red" } else { "Brightgreen" } if ($total -eq 0) { WriteLog "No tests results are available for this build" $content = "not available" $color = "red" } else { $values = @() $valuesHashTable = @{} if ($passed -gt 0) { $values += "$passed passed" $valuesHashTable["passed"] = $passed } if ($failed -gt 0) { $values += "$failed failed" $valuesHashTable["failed"] = $failed } if ($skipped -gt 0) { $values += "$skipped skipped" $valuesHashTable["skipped"] = $skipped } $script:pipelineInvocationResult["TestResults"] = $valuesHashTable WriteLog "Create test results badge" $content = $values -join " | " } NewBadge -Label "Tests" -Content $content -Color $color -FilePath $FilePath } function NewBadge { param ( [Parameter(Mandatory=$true)] [ValidateNotNull()] [String] $Label, [Parameter(Mandatory=$true)] [ValidateNotNull()] [String] $Content, [Parameter(Mandatory=$true)] [ValidateNotNull()] [String] $Color, [Parameter(Mandatory=$true)] [ValidateNotNull()] [String] $FilePath ) if (-not $FilePath.EndsWith(".svg")) { WriteLog -Message "The file extension must be .svg" -Throw } # If the directory does not exits, create it $folderPath = Split-Path $FilePath -Parent if (-not (test-path $folderPath)) { New-Item -Path $folderPath -Force -ItemType Directory | Out-Null } if (test-path $FilePath) { Remove-Item $FilePath -Force | Out-Null } Invoke-RestMethod https://img.shields.io/badge/$Label-$Content-$Color.svg -OutFile $FilePath -MaximumRetryCount 3 -RetryIntervalSec 1 -ErrorAction Stop } # Get the date in Pacific Standard Time # function GetDatePST { $now = Get-Date $timeZoneInfo = [TimeZoneInfo]::FindSystemTimeZoneById("Pacific Standard Time") $date = [TimeZoneInfo]::ConvertTime($now, $timeZoneInfo) return $date } function UploadFilesToStorageAccount { param ( [Parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] [System.String] $StorageAccountName, [Parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] [System.String] $StorageAccountKey, [Parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] [System.String] $SourcePath, [Parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] [System.String] $FunctionsVersion ) if (-not (Test-Path $SourcePath)) { WriteLog -Message "SourcePath '$SourcePath' does not exist." -Throw } WriteLog "Enumerating svg files... in '$SourcePath'" $filesToUpload = @(Get-ChildItem -Path "$SourcePath/*" -Include "*.txt", "*.svg", "*.json" | ForEach-Object {$_.FullName}) if ($filesToUpload.Count -eq 0) { WriteLog -Message "'$SourcePath' does not contain any svg or text files to upload." -Throw } # Install the storage module if needed if (-not (Get-command New-AzStorageContext -ea SilentlyContinue)) { WriteLog "Installing Az.Storage." Install-Module Az.Storage -Force -Verbose -AllowClobber -Scope CurrentUser } $context = $null try { WriteLog "Connecting to storage account..." $context = New-AzStorageContext -StorageAccountName $StorageAccountName -StorageAccountKey $StorageAccountKey -ErrorAction Stop } catch { $message = "Failed to authenticate with Azure. Please verify the StorageAccountName and StorageAccountKey. Exception information: $_" WriteLog -Message $message -Throw } $CONTAINER_NAME = "pipelineresults" $destinationPath = $null foreach ($file in $filesToUpload) { $fileName = Split-Path $file -Leaf $pipelineFolderName = Split-Path (Split-Path $file -Parent) -Leaf $destinationPath = [IO.Path]::Combine($FunctionsVersion, $pipelineFolderName, $fileName) $contentType = GetContentType -FilePath $file try { WriteLog -Message "Uploading '$fileName' to '$destinationPath'." Set-AzStorageBlobContent -File $file ` -Container $CONTAINER_NAME ` -Blob $destinationPath ` -Context $context ` -StandardBlobTier Hot ` -ErrorAction Stop ` -Properties @{"ContentType" = $contentType; "CacheControl" = "no-cache"} ` -Force | Out-Null } catch { WriteLog -Message "Failed to upload file '$file' to storage account. Exception information: $_" -Throw } } } function GetContentType { param ( [Parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] [System.String] $FilePath ) $fileExtension = [System.IO.Path]::GetExtension($FilePath) switch ($fileExtension) { ".json" { "application/json" } ".txt" { "text/plain" } ".svg" { "image/svg+xml" } default { "application/octet-stream"} } } Class PipelineDefinition { [String]$Name [String]$DisplayName [String]$Owner [Hashtable]$RequestBody [String]$Id [String]$Type } # Validate input parameters foreach ($varibleName in @("PipelineName", "DisplayName", "Owner", "PipelineDefinitionId", "SourceBranch", "PipelineType")) { $result = Get-Variable $varibleName -ErrorAction SilentlyContinue if (-not $result.Value) { WriteLog -Message "Variable name '$varibleName' cannot be null or empty." -Throw } } # Validate $PipelineType $validPipelineType = @("Build", "Test") if (-not $validPipelineType.Contains($PipelineType)) { $options = $validPipelineType -join ", " WriteLog -Message "'PipelineType' is invalid. Valid inputs are: $options" -Throw } # Create the request body with the pipeline parameters $pipelineParams = $null if ([string]::IsNullOrWhiteSpace($PipelineParameters) -or $PipelineParameters -eq "None") { $pipelineParams = @{} } else { $pipelineParams = ParsePipelineParameters -PipelineParameters $PipelineParameters } # For the Core Tools pipeline, add/replace the value for the IntegrationBuildNumber if ($PipelineDefinitionId -eq 11) { $parameterName = "IntegrationBuildNumber" $integrationBuildNumber = "PreRelease" + (GetDatePST).ToString("yyMMdd-HHmm") $pipelineParams[$parameterName] = $integrationBuildNumber } # Remove any empty spaces from the branch name $SourceBranch = $SourceBranch.Trim() $requestBody = @{ parameters = ( $pipelineParams | ConvertTo-Json ) definition = @{ id = $PipelineDefinitionId } sourceBranch = $SourceBranch } $pipelineDefinition = [PipelineDefinition]::new() $pipelineDefinition.Name = $PipelineName $pipelineDefinition.DisplayName = $DisplayName $pipelineDefinition.Owner = $Owner $pipelineDefinition.RequestBody = $requestBody $pipelineDefinition.Id = $PipelineDefinitionId $pipelineDefinition.Type = $PipelineType # This is used to hold the information results of the pipeline which gets written to a json file. $script:pipelineInvocationResult = @{ Name = $PipelineName DisplayName = $DisplayName Type = $PipelineType SourceBranch = $SourceBranch ExecutionTime = $null Status = $null TestResults = $null BuildUrl = $null BuildId = $null } InvokeDevOpsPipeline -PipelineDefinition $pipelineDefinition -StorageAccountName $storageAccountName -StorageAccountKey $storageAccountKey -FunctionsVersion $FunctionsVersion