Skip to content

Instantly share code, notes, and snippets.

@loopyd
Created May 17, 2023 10:56
Show Gist options
  • Save loopyd/a12da8a6b0171cd4156d7071ac4cbb7e to your computer and use it in GitHub Desktop.
Save loopyd/a12da8a6b0171cd4156d7071ac4cbb7e to your computer and use it in GitHub Desktop.
[PowerShell] Simple Azure DevOps pipeline executor
# 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