Created
May 17, 2023 10:56
-
-
Save loopyd/a12da8a6b0171cd4156d7071ac4cbb7e to your computer and use it in GitHub Desktop.
[PowerShell] Simple Azure DevOps pipeline executor
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Author: Robert Smith <loopyd@github> | Date Created: 2023-05-17 | License: DBAD Public License vv1.1 09-2016 | |
# | |
# Description: This script will create a pipeline in Azure DevOps, and run it. | |
# | |
# Usage: run.ps1 -Organization https://dev.azure.com/organization -Project ProjectName | |
# -Repository RepositoryName -RepositoryType github -Branch master -PATToken <PATToken> | |
# -PipelineName PipelineName -YAMLFilePath /path/to/yaml/file | |
# -ConfigFilePath /path/to/config/file -DeleteExistingPipeline $true -BuildStage BuildStageName | |
# | |
# Parameters: | |
# | |
# - Organization: The URL of the Azure DevOps organization | |
# - Project: The name of the project in Azure DevOps | |
# - Repository: The name of the repository in Azure DevOps (or a URL, for github) | |
# - RepositoryType: The type of repository in Azure DevOps (github, git, tfsgit, tfsversioncontrol) | |
# - Branch: The branch to run the pipeline on | |
# - PATToken: The PAT token to use to authenticate with Azure DevOps | |
# - PipelineName: The name of the pipeline to create | |
# - YAMLFilePath: The path to the YAML file to use for the pipeline | |
# - ConfigFilePath: The path to the config file to use for the pipeline | |
# - DeleteExistingPipeline: Whether or not to delete the existing pipeline before creating a new one | |
# - BuildStage: The name of the build stage to use in the config file | |
# | |
# The Config file: | |
# | |
# The config file is a JSON file that contains the configuration for the pipeline. It is used to store the | |
# configuration for the pipeline, so that it can be re-run without having to specify all the parameters again. | |
# It is created if it does not exist, and updated if it does exist | |
# | |
# The config file is structured as follows: | |
# | |
# { | |
# "buildPlan": { | |
# "BuildStageName1": { | |
# "pipelineName": "PipelineName", | |
# "yamlFilePath": "/path/to/yaml/file", | |
# "projectName": "ProjectName", | |
# "repositoryName": "RepositoryName", | |
# "repositoryType": "github", | |
# "branchName": "master" | |
# }, | |
# "BuildStageName2": { | |
# "pipelineName": "PipelineName", | |
# "yamlFilePath": "/path/to/yaml/file", | |
# "projectName": "ProjectName", | |
# "repositoryName": "RepositoryName", | |
# "repositoryType": "github", | |
# "branchName": "master" | |
# }, | |
# ... | |
# }, | |
# "client": { | |
# "patToken": "PATToken", | |
# "organizationUrl": "https://dev.azure.com/organization", | |
# "deleteExistingPipeline": true | |
# } | |
# } | |
# | |
# The config file is updated with the values from the parameters, and the values from the config file are used if the | |
# parameters are not specified. Thus you can shorten repeat runs with: | |
# | |
# run.ps1 -BuildStage BuildStageName -ConfigFilePath /path/to/config/file.json | |
# | |
# Support for the PAT token is included. If the PAT token is not specified, the script will fail. | |
param( | |
[Parameter(Mandatory = $false)] | |
[string]$Organization = $null, | |
[Parameter(Mandatory = $false)] | |
[string]$Project = $null, | |
[Parameter(Mandatory = $false)] | |
[string]$Repository = $null, | |
[Parameter(Mandatory = $false)] | |
[string]$RepositoryType = $null, | |
[Parameter(Mandatory = $false)] | |
[string]$Branch = $null, | |
[Parameter(Mandatory = $false)] | |
[string]$PATToken = $null, | |
[Parameter(Mandatory = $false)] | |
[string]$PipelineName = $null, | |
[Parameter(Mandatory = $false)] | |
[string]$YAMLFilePath = $null, | |
[Parameter(Mandatory = $false)] | |
[string]$ConfigFilePath = $null, | |
[Parameter(Mandatory = $false)] | |
[bool]$DeleteExistingPipeline = $false, | |
[Parameter(Mandatory = $true)] | |
[string]$BuildStage = $null | |
) | |
$global:BuildPlanProperties = @('pipelineName', 'yamlFilePath', 'projectName', 'repositoryName', 'repositoryType', 'branchName') | |
$global:ClientProperties = @('patToken', 'organizationUrl', 'deleteExistingPipeline') | |
function Save-Config { | |
param( | |
[Parameter(Mandatory = $true)] | |
[string]$BuildStage, | |
[Parameter(Mandatory = $true)] | |
$CurrentConfig, | |
[Parameter(Mandatory = $false)] | |
[string]$ConfigFilePath | |
) | |
# If the file exists, we should grab the structure of the file and its existing values | |
if (Test-Path $ConfigFilePath) { | |
$config = Get-Content -Path $ConfigFilePath | ConvertFrom-Json -Depth 4 | |
if ($config.buildPlan.PSObject.Properties.Name -notcontains $BuildStage) { | |
$config.buildPlan | Add-Member -Type NoteProperty -Name $BuildStage -Value (New-Object PSObject) | |
} | |
} else { | |
# If the file does not exist, we create a new config file structure | |
$config = New-Object PSObject | |
$config | Add-Member -Type NoteProperty -Name buildPlan -Value (New-Object PSObject) | |
$config.buildPlan | Add-Member -Type NoteProperty -Name $BuildStage -Value (New-Object PSObject) | |
$config | Add-Member -Type NoteProperty -Name client -Value (New-Object PSObject) | |
} | |
# We then update the config structure with the values from the current config, and save it | |
$CurrentConfig.PSObject.Properties | | |
ForEach-Object { | |
if ($_.Name -in $global:BuildPlanProperties) { | |
$config.buildPlan.$BuildStage | Add-Member -Type NoteProperty -Name $_.Name -Value $_.Value -Force | |
} | |
if ($_.Name -in $global:ClientProperties) { | |
$config.client | Add-Member -Type NoteProperty -Name $_.Name -Value $_.Value -Force | |
} | |
} | |
$config | ConvertTo-Json | Set-Content -Path $ConfigFilePath | |
} | |
function Get-Config { | |
param( | |
[Parameter(Mandatory = $true)] | |
[string]$BuildStage, | |
[Parameter(Mandatory = $true)] | |
$CurrentConfig, | |
[Parameter(Mandatory = $false)] | |
[string]$ConfigFilePath | |
) | |
if (Test-Path $ConfigFilePath) { | |
# If the file exists, we update the config with the values from the file by merging the two together | |
$config = Get-Content -Path $ConfigFilePath | ConvertFrom-Json | |
if ($config.buildPlan.PSObject.Properties.Name -notcontains $BuildStage) { | |
$config.buildPlan | Add-Member -Type NoteProperty -Name $BuildStage -Value (New-Object PSObject) | |
} | |
$CurrentConfig.PsObject.Properties | | |
ForEach-Object { | |
$currentProperty = $_.Name | |
if ($currentProperty -in $global:BuildPlanProperties -and [string]::IsNullOrEmpty($_.Value)) { | |
$_.Value = $config.buildPlan.$BuildStage.$currentProperty | |
} | |
if ($currentProperty -in $global:ClientProperties -and [string]::IsNullOrEmpty($_.Value)) { | |
$_.Value = $config.client.$currentProperty | |
} | |
} | |
} else { | |
# If the file does not exist, we create a new config file, and just return the config structure as-is | |
Save-Config -BuildStage $BuildStage -CurrentConfig $CurrentConfig -configFilePath $ConfigFilePath | |
} | |
return $CurrentConfig | |
} | |
function RunLogin() { | |
param( | |
[Parameter(Mandatory = $true)] | |
$CurrentConfig | |
) | |
if (-not [string]::IsNullOrEmpty($CurrentConfig.patToken)) { | |
$env:AZURE_DEVOPS_EXT_PAT = $clientConfig.patToken | |
} | |
$loggedIn = az account show 2>$null | |
if ($null -eq $loggedIn) { | |
az devops login --organization $CurrentConfig.organizationUrl --output none | |
} | |
if ($null -eq $loggedIn) { | |
Write-Host "Login failed, the script will now exit" | |
exit | |
} | |
} | |
function RunPipeline { | |
param( | |
[Parameter(Mandatory = $true)] | |
[string]$BuildStage, | |
[Parameter(Mandatory = $true)] | |
$CurrentConfig | |
) | |
$pipeline = az pipelines list --org $CurrentConfig.organizationUrl --project $CurrentConfig.projectName --output json | ConvertFrom-Json | Where-Object { $_.name -eq $CurrentConfig.pipelineName } | Select-Object -First 1 | |
if ($null -ne $pipeline -and $CurrentConfig.deleteExistingPipeline -eq $true) { | |
Write-Host "Deleting existing pipeline $($pipeline.name)..." | |
az pipelines delete --id $pipeline.id --organization $CurrentConfig.organizationUrl --project $CurrentConfig.projectName --yes | |
} | |
Write-Host "Creating pipeline $($CurrentConfig.pipelineName)..." | |
$pipeline = az pipelines create --name $CurrentConfig.pipelineName --org $CurrentConfig.organizationUrl --project $CurrentConfig.projectName --repository $CurrentConfig.repositoryName --repository-type $CurrentConfig.repositoryType --branch $CurrentConfig.branchName --yaml-path $CurrentConfig.yamlFilePath --skip-first-run | ConvertFrom-Json | |
Write-Host "Running pipeline $($pipeline.name) with branch $($CurrentConfig.branchName) and yaml file $($CurrentConfig.yamlFilePath)" | |
$build = az pipelines run --id $pipeline.id --org $CurrentConfig.organizationUrl --project $CurrentConfig.projectName --branch $CurrentConfig.branchName --output json | ConvertFrom-Json | |
$BuildMonitorThreadScriptBlock = { | |
param($buildId, $orgUrl, $projectName) | |
do { | |
$run = az pipelines runs show --id $buildId --org $orgUrl --project $projectName --query "status" -o tsv | |
Start-Sleep -Seconds 10 | |
} while ($run -eq "inProgress" -or $run -eq "notStarted" -or $run -eq "queued") | |
} | |
$BuildMonitorThread = Start-ThreadJob -ScriptBlock $BuildMonitorThreadScriptBlock -ArgumentList $build.id, $CurrentConfig.organizationUrl, $CurrentConfig.projectName | |
$startTime = Get-Date | |
Write-Host -NoNewline "`r$spinnerCharacter $formattedTime" | |
$spinner = "-\|/" | |
do { | |
$currentTime = Get-Date | |
$elapsedTime = $currentTime - $startTime | |
$formattedTime = '{0}h {1}m {2}s {3}ms' -f $elapsedTime.Hours, $elapsedTime.Minutes, $elapsedTime.Seconds, $elapsedTime.Milliseconds | |
$spinnerIndex = [math]::Floor(((Get-Date).Millisecond) / 250) % $spinner.Length | |
$spinnerCharacter = $spinner[$spinnerIndex] | |
Write-Host -NoNewline "`r$spinnerCharacter Build running: $formattedTime " | |
Start-Sleep -Milliseconds 5 | |
} while ($BuildMonitorThread.State -eq "Running" -or $BuildMonitorThread.State -eq "NotStarted") | |
$run = $BuildMonitorThread | Receive-Job | |
Write-Host $("`r" + $(" " * 80) + "`rBuild completed in $formattedTime") | |
} | |
$currentConfig = New-Object PSObject -Property @{ | |
"organizationUrl" = $Organization | |
"projectName" = $Project | |
"repositoryName" = $Repository | |
"repositoryType" = $RepositoryType | |
"patToken" = $PATToken | |
"pipelineName" = $PipelineName | |
"yamlFilePath" = $YAMLFilePath | |
"deleteExistingPipeline" = $DeleteExistingPipeline | |
"branchName" = $Branch | |
} | |
$currentConfig = Get-Config -BuildStage $BuildStage -CurrentConfig $currentConfig -ConfigFilePath $ConfigFilePath | |
RunLogin -CurrentConfig $currentConfig | |
RunPipeline -BuildStage $BuildStage -CurrentConfig $currentConfig | |
Save-Config -BuildStage $BuildStage -CurrentConfig $currentConfig -ConfigFilePath $ConfigFilePath |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment