AzDOCmd.psm1


<#
.SYNOPSIS
Gets information on an agent pool (or pools) in Azure Pipelines.
 
.DESCRIPTION
Gets information on an agent pool (or pools) in Azure Pipelines.
 
.PARAMETER Name
Name of the pool to get information on. All pools will be returned if nothing is specified.
 
.PARAMETER Pat
Personal access token authorized to administer builds. Defaults to $env:SYSTEM_ACCESSTOKEN for use in Azure
Pipelines.
 
.EXAMPLE
Get-AzDOAgentPool -Name 'Azure Pipelines'
 
.LINK
https://docs.microsoft.com/en-us/rest/api/azure/devops/distributedtask/pools/get%20agent%20pools
 
.NOTES
The Cmdlet will work as-is in a UI Pipeline with the default $Pat parameter as long as OAUTH access has been
enabled for the pipeline/job. If using a YAML build, the system.accesstoken variable needs to be explicitly
mapped to the steps environment like the following example:
 
steps:
- powershell: Invoke-WebRequest -Uri $Uri -Headers ( Initialize-AzDORestApi )
  env:
    SYSTEM_ACCESSTOKEN: $(system.accesstoken)
#>

function Get-AzDOAgentPool {
    [CmdletBinding()]
    param (
        [Parameter(Position = 0)]
        [String[]]$Name,
        [Switch]$NoRetry,
        [String]$CollectionUri = $env:SYSTEM_COLLECTIONURI,
        [String]$Pat = $env:SYSTEM_ACCESSTOKEN
    )

    begin {
        $script:AzApiHeaders = @{
            Headers       = Initialize-AzDORestApi -Pat $Pat
            CollectionUri = $CollectionUri
            ApiVersion    = '6.1'
        }
    }

    process {
        # TODO: figure out how to get agent pools from projects
        $restArgs = @{
            Method   = 'Get'
            Endpoint = 'distributedtask/pools'
            NoRetry  = $NoRetry
        }

        if ($Name) {
            foreach ($filter in $Name) {
                Write-Verbose -Message "Getting information for the $filter agent pool..."
                $restArgs['Params'] = "poolName=$filter"
                Invoke-AzDORestApiMethod @script:AzApiHeaders @restArgs
            }
        }
        else {
            Write-Verbose -Message 'Getting information for all agent pools...'
            Invoke-AzDORestApiMethod @script:AzApiHeaders @restArgs
        }
    }
}

<#
.SYNOPSIS
Gets user information based on the given PAT.
 
.DESCRIPTION
Gets user information based on the given PAT.
 
.PARAMETER CollectionUri
URI of the organization that the PAT belongs to.
 
.PARAMETER Pat
Azure DevOps personal access token.
 
.EXAMPLE
Get-AzDOPatIdentity
 
.NOTES
N/A
#>

function Get-AzDOPatIdentity {
    [CmdletBinding()]
    param (
        [Switch]$NoRetry,
        [String]$CollectionUri = $env:SYSTEM_COLLECTIONURI,
        [String]$Pat = $env:SYSTEM_ACCESSTOKEN
    )

    $script:AzApiHeaders = @{
        Headers       = Initialize-AzDORestApi -Pat $Pat
        CollectionUri = $CollectionUri
        ApiVersion    = '7.1-preview.1'
    }

    try {
        Invoke-AzDORestApiMethod `
            @script:AzApiHeaders `
            -Method 'Get' `
            -Endpoint 'connectionData' `
            -NoRetry:$NoRetry `
            -ErrorAction Stop
    }
    catch {
        Write-Warning -Message "No valid identity found for the given Personal Access Token at $CollectionUri"
    }
}

<#
.SYNOPSIS
Gets information for an Azure DevOps project.
 
.DESCRIPTION
Gets information for an Azure DevOps project.
 
.PARAMETER Name
Name of the project.
 
.PARAMETER CollectionUri
https://dev.azure.com/[organization]
 
.PARAMETER Pat
A personal access token authorized as a reader for the collection.
 
.EXAMPLE
Get-AzDOProject -Name MyProject
 
.LINK
https://docs.microsoft.com/en-us/rest/api/azure/devops/core/projects/get
 
.NOTES
N/A
#>

function Get-AzDOProject {
    [CmdletBinding()]
    param (
        [Parameter(Position = 0)]
        [String[]]$Name,
        [Switch]$NoRetry,
        [String]$CollectionUri = $env:SYSTEM_COLLECTIONURI,
        [String]$Pat = $env:SYSTEM_ACCESSTOKEN
    )

    begin {
        $script:AzApiHeaders = @{
            Headers       = Initialize-AzDORestApi -Pat $Pat
            CollectionUri = $CollectionUri
            ApiVersion    = '6.1'
        }

        $restParams = @{
            Method  = 'Get'
            Params  = @('includeCapabilities=true')
            NoRetry = $NoRetry
        }
    }

    process {
        if ($Name) {
            foreach ($ref in $Name) {
                Invoke-AzDORestApiMethod `
                    @script:AzApiHeaders `
                    @restParams `
                    -Endpoint "projects/$ref"
            }
        }
        else {
            Invoke-AzDORestApiMethod `
                @script:AzApiHeaders `
                @restParams `
                -Endpoint 'projects'
        }
    }
}

<#
.SYNOPSIS
Adds a NuGet package to a private feed.
 
.DESCRIPTION
Adds a NuGet package to a private feed.
 
.PARAMETER FilePath
Path to the NuGet package file.
 
.PARAMETER FeedName
Name of the feed to publish to.
 
.PARAMETER Project
The project that the package feed is scoped to. Leave out for organization-scoped feeds.
 
.PARAMETER CollectionUri
https://dev.azure.com/[organization]
 
.PARAMETER Pat
A personal access token authorized to push to feeds.
 
.EXAMPLE
Add-AzDOPackage -FilePath packagename.1.0.0.nupkg -FeedName MyFeed
 
.NOTES
N/A
#>

function Add-AzDOPackage {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [Alias('FullName')]
        [String[]]$FilePath,
        [Parameter(Mandatory = $true)]
        [String[]]$FeedName,
        [String]$Project,
        [String]$CollectionUri = $env:SYSTEM_COLLECTIONURI,
        [String]$Pat = $env:SYSTEM_ACCESSTOKEN
    )

    begin {
        # Workaround for using variables only in a ForEach-Object block
        Write-Information -MessageData (($Project + $CollectionUri + $Pat) -replace '.*', '' )

        $nuget = try {
            ( Get-Command -Name nuget ).Name
        }
        catch {
            ( Install-NugetCli ).Name
        }

        foreach ($feed in $FeedName) {
            try {
                Get-PackageSource -Name $feed -ErrorAction Stop | Out-Null
            }
            catch {
                if ( Get-AzDOPackageFeed -Name $feed -Project $Project -CollectionUri $CollectionUri -Pat $Pat ) {
                    $null = Register-AzDOPackageFeed -Name $feed -Project $Project -Force -Pat $Pat
                }
                else {
                    throw $_
                }
            }

            Start-CliProcess `
                -FilePath $nuget `
                -ArgumentList (
                    'sources', 'Update',
                    '-Name', $feed,
                    '-UserName', $Pat,
                    '-Password', $Pat
                ) `
                -WorkingDirectory $PWD
        }
    }

    process {
        foreach ($feed in $FeedName) {
            Get-Item -Path $FilePath |
                ForEach-Object -Process {
                    Start-CliProcess `
                        -FilePath $nuget `
                        -ArgumentList (
                            'push',
                            '-Source',
                            $feed,
                            '-ApiKey',
                            $Pat,
                            $_.FullName
                        ) `
                        -WorkingDirectory $PWD

                    $name, $version = $_.BaseName -split '(?<=[^\d])\.(?=\d)'
                    $uploadedPackage = Get-AzDOPackage `
                        -Feed $feed `
                        -PackageName $name `
                        -Version $version `
                        -CollectionUri $CollectionUri `
                        -Project $Project `
                        -Pat $Pat
                    if ($uploadedPackage) {
                        $uploadedPackage
                    }
                    else {
                        throw "$name upload unsuccessful."
                    }
                }
        }
    }
}

<#
.SYNOPSIS
Gets the details of a package in an Azure DevOps package feed.
 
.DESCRIPTION
Gets the details of a package in an Azure DevOps package feed.
 
.PARAMETER PackageName
Exact name of the package. If nothing is specified, it will return all packages in the feed.
 
.PARAMETER Version
Version of the package to get details for.
 
.PARAMETER Feed
Name of the feed that the package is in, or a feed object from Get-AgentPool.
 
.PARAMETER CollectionUri
https://dev.azure.com/[organization]
 
.PARAMETER Project
The project that the package feed is scoped to. Leave out for organization-scoped feeds.
 
.PARAMETER Destination
Downloads the package to the specified directory.
 
.PARAMETER Pat
A personal access token authorized to access feeds.
 
.EXAMPLE
Get-AzDOPackageFeed -PackageName ScmFeed | Get-AzDOPackage
 
.NOTES
N/A
 
.LINK
https://docs.microsoft.com/en-us/rest/api/azure/devops/artifactspackagetypes/nuget/download%20package
#>

function Get-AzDOPackage {
    [CmdletBinding(DefaultParameterSetName = 'List')]
    param (
        [String[]]$PackageName,
        [Parameter(ParameterSetName = 'List')]
        [Parameter(ParameterSetName = 'Download', Mandatory = $true)]
        [String[]]$Version,
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [Alias('name')]
        [System.Object]$Feed,
        [String]$CollectionUri = $env:SYSTEM_COLLECTIONURI,
        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [System.Object]$Project = @(''),
        [Parameter(ParameterSetName = 'Download', Mandatory = $true)]
        [String]$Destination,
        [Switch]$NoRetry,
        [String]$Pat = $env:SYSTEM_ACCESSTOKEN
    )

    begin {
        $script:AzApiHeaders = @{
            Headers       = Initialize-AzDORestApi -Pat $Pat
            CollectionUri = $CollectionUri
            ApiVersion    = '7.1-preview.1'
        }
    }

    process {
        . $PSScriptRoot/private/Get-AzDOApiProjectName.ps1
        $Project = $Project | Get-AzDOApiProjectName

        if ($Feed -is [String]) {
            $Feed = Get-AzDOPackageFeed `
                -Name $Feed `
                -Project $Project `
                -CollectionUri $CollectionUri `
                -Pat $Pat `
                -NoRetry:$NoRetry
        }

        $feedPackages = @()
        $feedPackages += foreach ($projectName in $Project) {
            Invoke-AzDORestApiMethod `
                @script:AzApiHeaders `
                -Method Get `
                -SubDomain 'feeds' `
                -Project $projectName `
                -Endpoint "packaging/Feeds/$($Feed.id)/packages" `
                -Params 'includeDescription=true' `
                -NoRetry:$NoRetry
        }
        if ($PackageName) {
            $namedPackage = foreach ($name in $PackageName) {
                foreach ($package in $feedPackages) {
                    if ($name -eq $package.name) {
                        $package | Add-Member -NotePropertyName feedId -NotePropertyValue $Feed.id
                        $package | Add-Member -NotePropertyName feedName -NotePropertyValue $Feed.name
                        $package
                    }
                }
            }
            foreach ($package in $namedPackage) {
                if ($Version) {
                    foreach ($packageVersion in $Version) {
                        $package.versions = Invoke-AzDORestApiMethod `
                            @script:AzApiHeaders `
                            -Method Get `
                            -SubDomain 'feeds' `
                            -Project $Feed.project.name `
                            -Endpoint "packaging/Feeds/$($Feed.id)/Packages/$($package.id)/versions" `
                            -NoRetry:$NoRetry |
                            Where-Object -Property version -EQ $packageVersion |
                            ForEach-Object -Process {
                                $versionObject = Invoke-AzDORestApiMethod `
                                    @script:AzApiHeaders `
                                    -Method Get `
                                    -SubDomain 'feeds' `
                                    -Project $Feed.project.name `
                                    -Endpoint (
                                        "packaging/Feeds/$($Feed.id)/Packages/$($package.id)/versions/$($_.id)"
                                    ) `
                                    -NoRetry:$NoRetry
                                [PSCustomObject]@{
                                    id                  = $_.id
                                    standardizedVersion = $_.normalizedVersion
                                    version             = $_.version
                                    isLatest            = $_.isLatest
                                    isListed            = $_.isListed
                                    storageId           = $_.storageId
                                    packageDescription  = $versionObject |
                                        Select-Object -ExpandProperty description
                                    views               = $_.views
                                    publishDate         = $_.publishDate
                                    }
                                } |
                                Select-Object -Property @(
                                    'id',
                                    'standardizedVersion',
                                    'version',
                                    'isLatest',
                                    'isListed',
                                    'storageId',
                                    'packageDescription',
                                    'views',
                                    'publishDate'
                                )
                        $package
                        if ($Destination) {
                            $outFile = "$env:TEMP/$($package.name).$($package.versions.standardizedVersion).zip"
                            Invoke-AzDORestApiMethod `
                                @script:AzApiHeaders `
                                -Method Get `
                                -SubDomain 'pkgs' `
                                -Project $Feed.project.name `
                                -Endpoint (
                                    "packaging/feeds/$($Feed.id)/nuget/packages/" +
                                    "$($package.name)/versions/$($package.versions.standardizedVersion)/content"
                                ) `
                                -OutFile $outFile `
                                -NoRetry:$NoRetry
                            $fullDestinationPath = (
                                "$Destination/$($package.name).$($package.versions.standardizedVersion)"
                            )
                            Expand-Archive -Path $outFile -DestinationPath $fullDestinationPath -Force
                            Get-Item -Path $fullDestinationPath
                        }
                    }
                }
                else {
                    $package
                }
            }
        }
        else {
            foreach ($package in $feedPackages) {
                $package | Add-Member -NotePropertyName feedId -NotePropertyValue $Feed.id
                $package | Add-Member -NotePropertyName feedName -NotePropertyValue $Feed.name
                $package
            }
        }
    }
}

<#
.SYNOPSIS
Gets information about an Azure DevOps package feed.
 
.DESCRIPTION
Gets information about an Azure DevOps package feed.
 
.PARAMETER Name
Name of the feed.
 
.PARAMETER Project
Project that the feed is scoped to. If nothing is specified, it will look for Organization-scoped feeds.
 
.PARAMETER CollectionUri
https://dev.azure.com/[organization]
 
.PARAMETER Pat
A personal access token authorized to access feeds.
 
.EXAMPLE
Get-AzDOPackageFeed -Name PulseFeed, ScmFeed
 
.NOTES
General notes
#>

function Get-AzDOPackageFeed {
    [CmdletBinding()]
    param (
        [String[]]$Name,
        [String[]]$Project = @(''),
        [Switch]$NoRetry,
        [String]$CollectionUri = $env:SYSTEM_COLLECTIONURI,
        [String]$Pat = $env:SYSTEM_ACCESSTOKEN
    )

    begin {
        $script:AzApiHeaders = @{
            Headers       = Initialize-AzDORestApi -Pat $Pat
            CollectionUri = $CollectionUri
            ApiVersion    = '5.1-preview.1'
        }
    }

    process {
        foreach ($projectName in $Project) {
            $allFeeds = @()
            $allFeeds += Invoke-AzDORestApiMethod `
                @script:AzApiHeaders `
                -Method Get `
                -SubDomain 'feeds' `
                -Project $projectName `
                -Endpoint 'packaging/feeds' `
                -NoRetry:$NoRetry
            foreach ($feed in $allFeeds) {
                $feed | Add-Member `
                    -MemberType NoteProperty `
                    -Name location `
                    -Value "$($CollectionUri)/_packaging/$($feed.name)/nuget/v3/index.json"
            }

            $orgName = $CollectionUri -replace 'https://dev.azure.com/', ''
            if (!$allFeeds) {
                $message = 'No feeds found in $orgName '
                if (![String]::isNullOrEmpty($projectName)) {
                    $message += "for project $projectName"
                }
                Write-Warning -Message $message
            }
            elseif ($Name) {
                $namedFeeds = $allFeeds | ForEach-Object -Process {
                    foreach ($feedName in $Name) {
                        if ($feedName -eq $_.name) {
                            $_
                        }
                    }
                }
                if ($namedFeeds) {
                    foreach ($namedFeed in $namedFeeds) {
                        $namedFeed
                    }
                }
                else {
                    $message = "No feeds named $($Name -join ', ') found in $orgName "
                    if (![String]::isNullOrEmpty($projectName)) {
                        $message += "for project $projectName"
                    }
                    Write-Warning -Message $message
                }
            }
            else {
                foreach ($feed in $allFeeds) {
                    $feed
                }
            }
        }
    }
}

<#
.SYNOPSIS
Gets the current retention policy of a feed.
 
.DESCRIPTION
Gets the current retention policy of a feed.
 
.PARAMETER Name
Name of the feed.
 
.PARAMETER Id
GUID of the feed.
 
.PARAMETER Project
Project that the feed is scoped to. If nothing is specified, it will look for Organization-scoped feeds.
 
.PARAMETER CollectionUri
https://dev.azure.com/[organization]
 
.PARAMETER Pat
A personal access token authorized to access feeds.
 
.EXAMPLE
Get-AzDOPackageFeed -Name MyFeed | Get-AzDOPackageFeedRetention
 
.LINK
https://docs.microsoft.com/en-us/rest/api/azure/devops/artifacts/retention-policies/get-retention-policy
 
.NOTES
N/A
#>

function Get-AzDOPackageFeedRetention {
    [CmdletBinding(SupportsShouldProcess = $true, DefaultParameterSetName = 'Name')]
    param (
        [Parameter(ParameterSetName = 'Name', Mandatory = $true)]
        [Alias('feed')]
        [String[]]
        $Name,

        [Parameter(ParameterSetName = 'ID', Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [String]
        $Id,

        [Switch]
        $NoRetry,

        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [System.Object]
        $Project,

        [String]
        $CollectionUri = $env:SYSTEM_COLLECTIONURI,

        [String]
        $Pat = $env:SYSTEM_ACCESSTOKEN
    )

    begin {
        $script:AzApiHeaders = @{
            Headers       = Initialize-AzDORestApi -Pat $Pat
            CollectionUri = $CollectionUri
            ApiVersion    = '7.1-preview.1'
        }
    }

    process {
        . $PSScriptRoot/private/Get-AzDOApiProjectName.ps1
        $Project = $Project | Get-AzDOApiProjectName

        if ($PSCmdlet.ParameterSetName -eq 'Name') {
            $Id = Get-AzDOPackageFeed `
                -Name $Name `
                -Project $Project `
                -NoRetry:$NoRetry `
                -CollectionUri $CollectionUri `
                -Pat $Pat |
                Select-Object `
                    -ExpandProperty id
        }
        foreach ($feedId in $Id) {
            Invoke-AzDORestApiMethod `
                @script:AzApiHeaders `
                -Method Get `
                -SubDomain 'feeds' `
                -Project $Project `
                -Endpoint "packaging/feeds/$feedId/retentionpolicies" `
                -NoRetry:$NoRetry `
                -WhatIf:$false `
                -Confirm:$false
        }
    }
}

<#
.SYNOPSIS
Creates a new feed for packages.
 
.DESCRIPTION
Creates a new feed for packages.
 
.PARAMETER Name
Name of the feed to create.
 
.PARAMETER Project
Project that the new feed will be scoped in. If Project is null or empty, the feed is created at the
"organization" scope.
 
.PARAMETER Pat
Personal access token authorized to administer Azure Artifacts. Defaults to $env:SYSTEM_ACCESSTOKEN for use in
Azure Pipelines.
 
.EXAMPLE
New-AzDOPackageFeed -FeedName MyFeed
 
.EXAMPLE
New-AzDOPackageFeed -FeedName MyFeed -Project MyProject
 
.NOTES
The Cmdlet will work as-is in a UI Pipeline with the default $Pat parameter as long as OAUTH access has been
enabled for the pipeline/job. If using a YAML build, the system.accesstoken variable needs to be explicitly
mapped to the steps environment like the following example:
 
steps:
- powershell: Invoke-WebRequest -Uri $Uri -Headers ( Initialize-AzDORestApi )
  env:
    SYSTEM_ACCESSTOKEN: $(system.accesstoken)
#>


function New-AzDOPackageFeed {
    [CmdletBinding(SupportsShouldProcess = $true)]
    param (
        [Parameter(Mandatory = $true)]
        [Alias('feed')]
        [String[]]$Name,
        [Switch]$NoRetry,
        [String]$Project,
        [String]$CollectionUri = $env:SYSTEM_COLLECTIONURI,
        [String]$Pat = $env:SYSTEM_ACCESSTOKEN
    )
    begin {
        $script:AzApiHeaders = @{
            Headers       = Initialize-AzDORestApi -Pat $Pat
            CollectionUri = $CollectionUri
            ApiVersion    = '5.1-preview.1'
        }
    }

    process {
        foreach ($feed in $Name) {
            $body = @{
                name = $feed
            } | ConvertTo-Json | Out-String

            $newFeed = Invoke-AzDORestApiMethod `
                @script:AzApiHeaders `
                -Method Post `
                -SubDomain 'feeds' `
                -Project $Project `
                -Endpoint 'packaging/feeds' `
                -Body $body `
                -NoRetry:$NoRetry
            $location = $CollectionUri
            if ($location[-1] -ne '/') {
                $location += '/'
            }
            if (![String]::IsNullOrEmpty($Project)) {
                $location += "$Project/"
            }
            $location += "_packaging/$($newFeed.name)/nuget/v3/index.json"
            $newFeed | Add-Member `
                -MemberType NoteProperty `
                -Name location `
                -Value $location
            $newFeed
        }
    }
}

<#
.SYNOPSIS
Registers an Azure Artifacts package feed.
 
.DESCRIPTION
Registers an Azure Artifacts package feed.
 
.PARAMETER Name
Name of the feed to register.
 
.PARAMETER Location
URL of the package feed source.
 
.PARAMETER Force
Register the source even if it exists.
 
.PARAMETER Pat
An Azure DevOps Personal Access Token authorized to access Azure Artifacts.
 
.EXAMPLE
Register-AzDOPackageFeed `
    -Name MyFeed `
    -Location https://pkgs.dev.azure.com/MyOrg/_packaging/MyFeed/nuget/v3/index.json
 
.NOTES
N/A
#>


function Register-AzDOPackageFeed {
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')]
    [CmdletBinding(SupportsShouldProcess = $true, DefaultParameterSetName = 'Location')]
    param (
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [Alias('feed')]
        [String[]]$Name,
        [Parameter(ParameterSetName = 'Location', ValueFromPipelineByPropertyName = $true)]
        [String]$Location,
        [Parameter(ParameterSetName = 'Name')]
        [ValidateSet(2, 3)]
        [String]$FeedVersion = 3,
        [Switch]$Force,
        [Parameter(ParameterSetName = 'Name')]
        [String]$Project,
        [String]$CollectionUri = $env:SYSTEM_COLLECTIONURI,
        [String]$Pat = $env:SYSTEM_ACCESSTOKEN
    )

    begin {
        Set-EnvironmentVariable `
            -Name NUGET_CREDENTIALPROVIDER_SESSIONTOKENCACHE_ENABLED `
            -Value true `
            -Scope User `
            -Force
        $env:NUGET_CREDENTIALPROVIDER_SESSIONTOKENCACHE_ENABLED = 'true'

        Update-AzDOAccessToken -Pat $Pat -System $false -CmTools $false
    }

    process {
        foreach ($feedName in $Name) {
            if (!$Location) {
                if ($FeedVersion -eq 3) {
                    $fullLocation = $CollectionUri.Replace('https://', 'https://pkgs.')
                    if ($fullLocation[-1] -ne '/') {
                        $fullLocation += '/'
                    }
                }
                else {
                    $orgName = $CollectionUri.Replace('https://dev.azure.com/', '').Replace('/', '').ToLower()
                    $fullLocation = "https://$($orgName).pkgs.visualstudio.com/"
                }
                if (![String]::isNullOrEmpty($Project)) {
                    $fullLocation += "$Project/"
                }
                if ($FeedVersion -eq 3) {
                    $fullLocation += "_packaging/$feedName/nuget/v3/index.json"
                }
                else {
                    $fullLocation += "_packaging/$feedName/nuget/v2"
                }
            }
            else {
                $fullLocation = $Location
            }

            Register-PackageSource `
                -Name $feedName `
                -Location $fullLocation `
                -ProviderName NuGet `
                -Credential ( Get-PatPSCredential -Pat $Pat ) `
                -Trusted `
                -Force:$Force
        }

        <# Method for authorizing external feeds. Not needed, but want to keep in case of future use.
        $endpointCredentials = @()
        if ($env:VSS_NUGET_EXTERNAL_FEED_ENDPOINTS) {
            try {
                $endpointCredentials += ( $env:VSS_NUGET_EXTERNAL_FEED_ENDPOINTS | ConvertFrom-Json ) |
                    Select-Object -ExpandProperty endpointCredentials
            }
            catch { }
        }
 
        $password = $(
            [Convert]::ToBase64String(
                [Text.Encoding]::ASCII.GetBytes($Pat)
            )
        )
        $newEndpoint = [PSCustomObject]@{
            endpoint = $Location
            username = ''
            password = $password
        }
        $newEndpoints = @()
        $newEndpoints += foreach ($endpoint in $endpointCredentials) {
            if ($endpoint -ne $newEndpoint['endpoint']) {
                $endpoint
            }
        }
        $newEndpoints += $newEndpoint
        $endpoints = [PSCustomObject]@{
            endpointCredentials = @($newEndpoints)
        }
        $endpointsJson = $endpoints | ConvertTo-Json | Out-String
        Set-EnvironmentVariable `
            -Name VSS_NUGET_EXTERNAL_FEED_ENDPOINTS `
            -Value $endpointsJson `
            -Scope User `
            -Force
        $env:VSS_NUGET_EXTERNAL_FEED_ENDPOINTS = $endpointsJson
        #>

    }
}

<#
.SYNOPSIS
Removes a package version from an Azure Artifacts feed.
 
.DESCRIPTION
Removes a package version from an Azure Artifacts feed.
 
.PARAMETER Name
Name of the package to remove.
 
.PARAMETER Version
Version of the named package to remove.
 
.PARAMETER Feed
Name or ID of the feed that the package is in.
 
.PARAMETER Provider
The package provider. (e.g. Nuget, npm)
 
.PARAMETER Project
Project that the package's feed is scoped to. If nothing is specified it will look for organization-scoped feed.
 
.PARAMETER Pat
An Azure DevOps Personal Access Token authorized to administer Azure Artifacts feeds.
 
.PARAMETER Force
Don't prompt for confirmation to remove the package.
 
.EXAMPLE
Get-AzDOPackage -PackageName PSSiOps -Version 9.9.9 -Feed MyFeed | Remove-AzDOPackage
 
.NOTES
N/A
#>

function Remove-AzDOPackage {
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')]
    [CmdletBinding(SupportsShouldProcess = $true)]
    param (
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [Alias('name')]
        [String[]]$PackageName,

        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [Alias('versions')]
        [System.Object[]]$Version,

        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [Alias('feedId')]
        [System.Object]$Feed,

        [ValidateSet('nuget', 'upack', 'npm')]
        [String]$Provider = 'nuget',

        [Switch]
        $Force,

        [Switch]
        $NoRetry,

        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [AllowNull()]
        [AllowEmptyString()]
        [System.Object]$Project,

        [String]$CollectionUri = $env:SYSTEM_COLLECTIONURI,

        [String]$Pat = $env:SYSTEM_ACCESSTOKEN
    )

    begin {
        $script:AzApiHeaders = @{
            Headers       = Initialize-AzDORestApi -Pat $Pat
            CollectionUri = $CollectionUri
            ApiVersion    = '5.1-preview.1'
        }
        [Regex]$GuidRegex = '(?im)^[{(]?[0-9A-F]{8}[-]?(?:[0-9A-F]{4}[-]?){3}[0-9A-F]{12}[)}]?$'
    }

    process {
        . $PSScriptRoot/private/Get-AzDOApiProjectName.ps1
        $Project = $Project | Get-AzDOApiProjectName
        if ($null -eq $Project) {
            $Project = @('')
        }

        foreach ($name in $PackageName) {
            foreach ($versionNumber in $Version) {
                if ($Feed -notmatch $GuidRegex) {
                    $feedId = Get-AzDOPackage `
                        -PackageName $name `
                        -Version $versionNumber `
                        -Feed $Feed `
                        -Project $Project `
                        -CollectionUri $CollectionUri `
                        -Pat $Pat `
                        -NoRetry:$NoRetry |
                        Select-Object -ExpandProperty feedId
                }
                else {
                    $feedId = $Feed
                }
                if ($versionNumber -isnot [String]) {
                    $versionNumber = $versionNumber.version
                }

                $endpoint = "packaging/feeds/$feedId/$Provider"
                if ($Provider -ne 'npm') {
                    $endpoint += '/packages'
                }
                $endpoint += "/$name/versions/$versionNumber"
                $deleteArgs = @{
                    Method    = 'Delete'
                    Subdomain = 'pkgs'
                    Endpoint  = $endpoint
                    NoRetry   = $NoRetry
                    WhatIf    = $false
                    Confirm   = $false
                }
                if ($Provider -eq 'nuget' -and ![String]::IsNullOrEmpty($Project)) {
                    $deleteArgs['Project'] = $Project
                }
                if (!$Force) {
                    $ConfirmPreference = 'Low'
                }
                if ($PSCmdlet.ShouldProcess("Feed $Feed", "Delete $name $versionNumber")) {
                    Invoke-AzDORestApiMethod @script:AzApiHeaders @deleteArgs
                }
            }
        }
    }
}

<#
.SYNOPSIS
Removes an Azure Artifacts package feed.
 
.DESCRIPTION
Removes an Azure Artifacts package feed.
 
.PARAMETER Name
Name of the feed to remove.
 
.PARAMETER Project
Project that the feed to remove is in. If no project is specified, it will look for an organization-scoped feed.
 
.PARAMETER CollectionUri
The project collection URL (https://dev.azure.com/[orgranization]).
 
.PARAMETER Force
Force feed removal without confirmation.
 
.PARAMETER Pat
Personal access token authorized to administer Azure Artifacts. Defaults to $env:SYSTEM_ACCESSTOKEN for use in
Azure Pipelines.
 
.EXAMPLE
Remove-AzDOPackageFeed -FeedName MyFeed
 
.EXAMPLE
Remove-AzDOPackageFeed -FeedName MyFeed -Project MyProject
 
.NOTES
The Cmdlet will work as-is in a UI Pipeline with the default $Pat parameter as long as OAUTH access has been
enabled for the pipeline/job. If using a YAML build, the system.accesstoken variable needs to be explicitly
mapped to the steps environment like the following example:
 
steps:
- powershell: Invoke-WebRequest -Uri $Uri -Headers ( Initialize-AzDORestApi )
  env:
    SYSTEM_ACCESSTOKEN: $(system.accesstoken)
#>


function Remove-AzDOPackageFeed {
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')]
    [CmdletBinding(SupportsShouldProcess = $true, DefaultParameterSetName = 'Name')]
    param (
        [Parameter(ParameterSetName = 'Name', Mandatory = $true)]
        [Alias('feed')]
        [String[]]$Name,
        [Parameter(ParameterSetName = 'ID', Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [String]$Id,
        [Switch]$Force,
        [Switch]$NoRetry,
        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [System.Object]$Project,
        [String]$CollectionUri = $env:SYSTEM_COLLECTIONURI,
        [String]$Pat = $env:SYSTEM_ACCESSTOKEN
    )
    begin {
        $script:AzApiHeaders = @{
            Headers       = Initialize-AzDORestApi -Pat $Pat
            CollectionUri = $CollectionUri
            ApiVersion    = '7.1-preview.1'
        }
    }

    process {
        . $PSScriptRoot/private/Get-AzDOApiProjectName.ps1
        $Project = $Project | Get-AzDOApiProjectName
        if ($null -eq $Project) {
            [String]$Project = @('')
        }

        if ($PSCmdlet.ParameterSetName -eq 'Name') {
            $Id = Get-AzDOPackageFeed `
                -Name $Name `
                -Project $Project `
                -CollectionUri $CollectionUri `
                -Pat $Pat `
                -NoRetry:$NoRetry |
                Select-Object `
                    -ExpandProperty id
        }
        if (!$Force) {
            $ConfirmPreference = 'Low'
        }
        foreach ($feedId in $Id) {
            if ($PSCmdlet.ShouldProcess($CollectionUri, "Remove feed $Name - $feedId")) {
                Invoke-AzDORestApiMethod `
                    @script:AzApiHeaders `
                    -Method Delete `
                    -SubDomain 'feeds' `
                    -Project $Project `
                    -Endpoint "packaging/feeds/$feedId" `
                    -NoRetry:$NoRetry `
                    -WhatIf:$false `
                    -Confirm:$false
            }
        }
    }
}

<#
.SYNOPSIS
Removes a retention policy for a package feed.
 
.DESCRIPTION
Removes a retention policy for a package feed.
 
.PARAMETER Name
Name of the feed.
 
.PARAMETER Id
GUID of the feed.
 
.PARAMETER Project
Project that the feed is scoped to. If nothing is specified, it will look for Organization-scoped feeds.
 
.PARAMETER CollectionUri
https://dev.azure.com/[organization]
 
.PARAMETER Force
Bypass confirmation.
 
.PARAMETER Pat
A personal access token authorized to access feeds.
 
.EXAMPLE
Get-AzDOPackageFeed -Name MyFeed | Set-AzDOPackageFeedRetention
 
.LINK
https://docs.microsoft.com/en-us/rest/api/azure/devops/artifacts/retention-policies/delete-retention-policy
 
.NOTES
N/A
#>

function Remove-AzDOPackageFeedRetention {
    [CmdletBinding(SupportsShouldProcess = $true, DefaultParameterSetName = 'Name')]
    param (
        [Parameter(ParameterSetName = 'Name', Mandatory = $true)]
        [Alias('feed')]
        [String[]]
        $Name,

        [Parameter(ParameterSetName = 'ID', Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [String]
        $Id,

        [Switch]
        $NoRetry,

        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [System.Object]
        $Project,

        [String]
        $CollectionUri = $env:SYSTEM_COLLECTIONURI,

        [Switch]
        $Force,

        [String]
        $Pat = $env:SYSTEM_ACCESSTOKEN
    )

    begin {
        $script:AzApiHeaders = @{
            Headers       = Initialize-AzDORestApi -Pat $Pat
            CollectionUri = $CollectionUri
            ApiVersion    = '7.1-preview.1'
        }
    }

    process {
        . $PSScriptRoot/private/Get-AzDOApiProjectName.ps1
        $Project = $Project | Get-AzDOApiProjectName

        if ($PSCmdlet.ParameterSetName -eq 'Name') {
            $Id = Get-AzDOPackageFeed `
                -Name $Name `
                -Project $Project `
                -CollectionUri $CollectionUri `
                -Pat $Pat `
                -NoRetry:$NoRetry |
                Select-Object `
                    -ExpandProperty id
        }
        if (!$Force) {
            $ConfirmPreference = 'Low'
        }
        foreach ($feedId in $Id) {
            if ($PSCmdlet.ShouldProcess($feedId, 'Remove feed retention policy.')) {
                Invoke-AzDORestApiMethod `
                    @script:AzApiHeaders `
                    -Method Delete `
                    -SubDomain 'feeds' `
                    -Project $Project `
                    -Endpoint "packaging/feeds/$feedId/retentionpolicies" `
                    -NoRetry:$NoRetry `
                    -WhatIf:$false `
                    -Confirm:$false
            }
        }
    }
}

<#
.SYNOPSIS
Sets a retention policy for a package feed.
 
.DESCRIPTION
Sets a retention policy for a package feed.
 
.PARAMETER Name
Name of the feed.
 
.PARAMETER Id
GUID of the feed.
 
.PARAMETER Count
Maximum versions to preserve per package and package type.
 
.PARAMETER Days
Number of days to preserve a package version after its latest download.
 
.PARAMETER Project
Project that the feed is scoped to. If nothing is specified, it will look for Organization-scoped feeds.
 
.PARAMETER CollectionUri
https://dev.azure.com/[organization]
 
.PARAMETER Force
Bypass confirmation.
 
.PARAMETER Pat
A personal access token authorized to access feeds.
 
.EXAMPLE
Get-AzDOPackageFeed -Name MyFeed | Set-AzDOPackageFeedRetention
 
.LINK
https://docs.microsoft.com/en-us/rest/api/azure/devops/artifacts/retention-policies/set-retention-policy
 
.NOTES
N/A
#>

function Set-AzDOPackageFeedRetention {
    [CmdletBinding(SupportsShouldProcess = $true, DefaultParameterSetName = 'Name')]
    param (
        [Parameter(ParameterSetName = 'Name', Mandatory = $true)]
        [Alias('feed')]
        [String[]]
        $Name,

        [Parameter(ParameterSetName = 'ID', Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [String]
        $Id,

        [Int]
        $Count = 5000,

        [Int]
        $Days = 365,

        [Switch]$NoRetry,

        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [System.Object]
        $Project,

        [String]
        $CollectionUri = $env:SYSTEM_COLLECTIONURI,

        [Switch]
        $Force,

        [String]
        $Pat = $env:SYSTEM_ACCESSTOKEN
    )

    begin {
        $script:AzApiHeaders = @{
            Headers       = Initialize-AzDORestApi -Pat $Pat
            CollectionUri = $CollectionUri
            ApiVersion    = '7.1-preview.1'
        }
    }

    process {
        . $PSScriptRoot/private/Get-AzDOApiProjectName.ps1
        $Project = $Project | Get-AzDOApiProjectName

        if ($PSCmdlet.ParameterSetName -eq 'Name') {
            $Id = Get-AzDOPackageFeed `
                -Name $Name `
                -Project $Project `
                -CollectionUri $CollectionUri `
                -Pat $Pat `
                -NoRetry:$NoRetry |
                Select-Object `
                    -ExpandProperty id
        }
        if (!$Force) {
            $ConfirmPreference = 'Low'
        }
        foreach ($feedId in $Id) {
            $shouldProcessPrompt = (
                "Set feed retention policy. Package count limit: $Count, " +
                "days to keep recently downloaded packages: $Days"
            )
            if ($PSCmdlet.ShouldProcess($feedId, $shouldProcessPrompt)) {
                $body = [PSCustomObject]@{
                    countLimit                           = $Count
                    daysToKeepRecentlyDownloadedPackages = $Days
                } | ConvertTo-Json

                Invoke-AzDORestApiMethod `
                    @script:AzApiHeaders `
                    -Method Put `
                    -SubDomain 'feeds' `
                    -Project $Project `
                    -Endpoint "packaging/feeds/$feedId/retentionpolicies" `
                    -Body $body `
                    -NoRetry:$NoRetry `
                    -WhatIf:$false `
                    -Confirm:$false
            }
        }
    }
}

<#
.SYNOPSIS
Adds a tag to a build.
 
.DESCRIPTION
Adds a tag to a build, or multiple tags.
 
.PARAMETER Tag
Tag(s) to add to a build.
 
.PARAMETER BuildId
ID of the build to add tags to.
 
.PARAMETER Project
Project that the build's pipeline resides in.
 
.PARAMETER CollectionUri
The Project Collection URI (https://dev.azure.com/[organization])
 
.PARAMETER Pat
An Azure DevOps Personal Access Token authorized to manage pipelines.
 
.EXAMPLE
Get-AzDOPipelineRun -Id 11111 -Project Tools | Add-AzDOPipelineRunTag tag1
Adds tag1 to build 11111
 
.NOTES
N/A
 
.LINK
https://docs.microsoft.com/en-us/rest/api/azure/devops/build/tags/add%20build%20tag
https://docs.microsoft.com/en-us/rest/api/azure/devops/build/tags/add%20build%20tags
#>

function Add-AzDOPipelineRunTag {
    [CmdletBinding(SupportsShouldProcess = $true)]
    param (
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, Position = 0)]
        [Alias('Tags')]
        [String[]]$Tag,
        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [Alias('Id')]
        [String[]]
        $BuildId = $env:BUILD_BUILDID,
        [Switch]$NoRetry,
        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [System.Object]
        $Project = $env:SYSTEM_TEAMPROJECT,
        [String]
        $CollectionUri = $env:SYSTEM_COLLECTIONURI,
        [String]
        $Pat = $env:SYSTEM_ACCESSTOKEN
    )

    begin {
        $script:AzApiHeaders = @{
            Headers       = Initialize-AzDORestApi -Pat $Pat
            CollectionUri = $CollectionUri
            ApiVersion    = '5.1'
        }
    }

    process {
        . $PSScriptRoot/private/Get-AzDOApiProjectName.ps1
        $Project = $Project | Get-AzDOApiProjectName

        foreach ($id in $buildId) {
            if ($Tag.Count -eq 1) {
                Invoke-AzDORestApiMethod `
                    @script:AzApiHeaders `
                    -Method Put `
                    -Project $Project `
                    -Endpoint "build/builds/$id/tags/$Tag" `
                    -NoRetry:$NoRetry `
                    -WhatIf:$WhatIfPreference
            }
            else {
                Invoke-AzDORestApiMethod `
                    @script:AzApiHeaders `
                    -Method Post `
                    -Project $Project `
                    -Endpoint "build/builds/$id/tags" `
                    -Body ( $Tag | ConvertTo-Json ) `
                    -NoRetry:$NoRetry `
                    -WhatIf:$WhatIfPreference
            }
        }
    }
}

<#
.SYNOPSIS
Exports a pipeline definition's json file.
 
.DESCRIPTION
Exports a pipeline definition's json file.
 
.PARAMETER PipelineDefinition
A pipeline definition passed via the pipeline from Get-AzDOPipeline.
 
.PARAMETER Destination
Destination folder of the json backup files.
 
.PARAMETER Pat
Personal access token authorized to administer pipelines and releases. Defaults to $env:SYSTEM_ACCESSTOKEN for use
in Azure Pipelines.
 
.EXAMPLE
Get-AzDOPipeline -Project Packages -Name AzurePipeline* | Export-AzDOPipeline
 
.NOTES
The Cmdlet will work as-is in a UI Pipeline with the default $Pat parameter as long as OAUTH access has been
enabled for the pipeline/job. If using a YAML build, the system.accesstoken variable needs to be explicitly
mapped to the steps environment like the following example:
 
steps:
- powershell: Invoke-WebRequest -Uri $Uri -Headers ( Initialize-AzDORestApi )
  env:
    SYSTEM_ACCESSTOKEN: $(system.accesstoken)
#>


function Export-AzDOPipeline {
    [CmdletBinding()]
    param (
        [Parameter(ValueFromPipeline = $true)]
        [System.Object[]]$PipelineDefinition,
        [string]$Destination = 'azure-pipelines',
        [string]$Pat = $env:SYSTEM_ACCESSTOKEN
    )

    begin {
        $script:AzApiHeaders = @{
            Headers = Initialize-AzDORestApi -Pat $Pat
        }

        $null = New-Item -Path $Destination -ItemType Directory -Force
    }

    process {
        foreach ($definition in $PipelineDefinition) {
            $outFileName = "$Destination/$($definition.name).json"
            Invoke-WebRequest -Uri $definition.url -Headers $script:AzApiHeaders['Headers'] -UseBasicParsing |
                Select-Object -ExpandProperty Content |
                Out-File -FilePath $outFileName -Encoding UTF8 -Force
            Get-Item -Path $outFileName
        }
    }
}

<#
.SYNOPSIS
Gets a build definition object from Azure Pipelines.
 
.DESCRIPTION
Gets a build definition object from Azure Pipelines using a project and name filter.
 
.PARAMETER Name
A filter to search for pipeline names.
 
.PARAMETER Id
The pipeline ID to get.
 
.PARAMETER Project
Project that the pipelines reside in.
 
.PARAMETER CollectionUri
The project collection URL (https://dev.azure.com/[orgranization]).
 
.PARAMETER Pat
Personal access token authorized to administer builds. Defaults to $env:SYSTEM_ACCESSTOKEN for use in Azure
Pipelines.
 
.EXAMPLE
Get-AzDOPipeline -Project Packages -Name AzurePipeline*
 
.LINK
https://docs.microsoft.com/en-us/rest/api/azure/devops/pipelines/pipelines/get
 
.LINK
https://docs.microsoft.com/en-us/rest/api/azure/devops/pipelines/pipelines/list
 
.NOTES
The Cmdlet will work as-is in a UI Pipeline with the default $Pat parameter as long as OAUTH access has been
enabled for the pipeline/job. If using a YAML build, the system.accesstoken variable needs to be explicitly
mapped to the steps environment like the following example:
 
steps:
- powershell: Invoke-WebRequest -Uri $Uri -Headers ( Initialize-AzDORestApi )
  env:
    SYSTEM_ACCESSTOKEN: $(system.accesstoken)
#>


function Get-AzDOPipeline {
    [CmdletBinding(DefaultParameterSetName = 'Name')]
    param (
        [Parameter(ParameterSetName = 'Name', Position = 0)]
        [String[]]$Name,
        [Parameter(ParameterSetName = 'Id', Position = 0)]
        [Int[]]$Id,
        [Switch]$NoRetry,
        [String[]]$Project = $env:SYSTEM_TEAMPROJECT,
        [String]$CollectionUri = $env:SYSTEM_COLLECTIONURI,
        [string]$Pat = $env:SYSTEM_ACCESSTOKEN
    )

    begin {
        $script:AzApiHeaders = @{
            Headers       = Initialize-AzDORestApi -Pat $Pat
            CollectionUri = $CollectionUri
            ApiVersion    = '6.1'
        }
    }

    process {
        if ($Id) {
            foreach ($projectName in $Project) {
                $pipeline = Invoke-AzDORestApiMethod `
                    @script:AzApiHeaders `
                    -Method Get `
                    -Project $projectName `
                    -Endpoint 'build/definitions' `
                    -Params "definitionIds=$($Id -join ',')" `
                    -NoRetry:$NoRetry `
                    -WhatIf:$false
                if ($pipeline) {
                    $pipeline
                }
                else {
                    Write-Warning -Message "Pipeline $Id not found in $projectName."
                }
            }
        }
        elseif ($Name) {
            foreach ($filter in $Name) {
                foreach ($projectName in $Project) {
                    $pipelineResponse = Invoke-AzDORestApiMethod `
                        @script:AzApiHeaders `
                        -Method Get `
                        -Project $projectName `
                        -Endpoint 'build/definitions' `
                        -Params "name=$filter" `
                        -NoRetry:$NoRetry `
                        -WhatIf:$false
                    if ($pipelineResponse) {
                        $pipelineResponse
                    }
                    else {
                        Write-Warning -Message "No pipelines found matching '$filter' in $projectName."
                    }
                }
            }
        }
        else {
            foreach ($projectName in $Project) {
                $pipelineResponse = Invoke-AzDORestApiMethod `
                    @script:AzApiHeaders `
                    -Method Get `
                    -Project $projectName `
                    -Endpoint 'build/definitions' `
                    -NoRetry:$NoRetry `
                    -WhatIf:$false
                if ($pipelineResponse) {
                    $pipelineResponse
                }
                else {
                    Write-Warning -Message "No pipelines found in $projectName."
                }
            }
        }
    }
}

<#
.SYNOPSIS
Gets details for a specific build by ID or user email.
 
.DESCRIPTION
Gets details for a specific build by ID or user email, with filters for build result, status, and reason.
 
.PARAMETER BuildId
The ID of the build to get.
 
.PARAMETER User
The email of the user that the build(s) to get was requested for.
 
.PARAMETER Result
Only return builds with the specified result.
 
.PARAMETER Status
Only return builds with the specified status.
 
.PARAMETER Reason
Only return builds with the specified reason.
 
.PARAMETER MaxBuilds
The maximum number of builds per project to get. Defaults to 10.
 
.PARAMETER Project
Project that the build's pipeline resides in.
 
.PARAMETER Pat
Personal access token authorized to administer builds. Defaults to $env:SYSTEM_ACCESSTOKEN for use in Azure
Pipelines.
 
.EXAMPLE
Get-AzDOPipelineRun -BuildId 111111 -Project MyProject
Gets the build with the specified ID.
 
.EXAMPLE
Get-AzDOPipelineRun -User myorg@dev.azure.com -Status completed -Reason buildCompletion
Gets 10 completed builds that were triggered by another build's completion started by myorg@dev.azure.com.
 
.NOTES
The Cmdlet will work as-is in a UI Pipeline with the default $Pat parameter as long as OAUTH access has been
enabled for the pipeline/job. If using a YAML build, the system.accesstoken variable needs to be explicitly
mapped to the steps environment like the following example:
 
steps:
- powershell: Invoke-WebRequest -Uri $Uri -Headers ( Initialize-AzDORestApi )
  env:
    SYSTEM_ACCESSTOKEN: $(system.accesstoken)
.LINK
https://docs.microsoft.com/en-us/rest/api/azure/devops/build/builds/get?view=azure-devops-rest-6.0
#>


function Get-AzDOPipelineRun {
    [CmdletBinding(DefaultParameterSetName = 'BuildId')]
    param (
        [Parameter(
            ValueFromPipelineByPropertyName = $true,
            Mandatory = $true,
            Position = 1,
            ParameterSetName = 'BuildId'
        )]
        [Alias('id')]
        [Int[]]
        $BuildId,

        [Parameter(
            Mandatory = $true,
            ParameterSetName = 'User'
        )]
        [Alias('requestedFor')]
        [String[]]
        $User,

        [Parameter(
            ParameterSetName = 'User'
        )]
        [ValidateSet(
            'canceled',
            'failed',
            'none',
            'partiallySucceeded',
            'succeeded'
        )]
        [String[]]
        $Result,

        [Parameter(
            ParameterSetName = 'User'
        )]
        [ValidateSet(
            'all',
            'cancelling',
            'completed',
            'inProgress',
            'none',
            'notStarted',
            'postponed'
        )]
        [String[]]
        $Status,

        [Parameter(
            ParameterSetName = 'User'
        )]
        [ValidateSet(
            'all',
            'batchedCI',
            'buildCompletion',
            'checkInShelveset',
            'individualCI',
            'manual',
            'none',
            'pullRequest',
            'resourceTrigger',
            'schedule',
            'scheduleForced',
            'triggered',
            'userCreated',
            'validateShelveset'
        )]
        [String[]]
        $Reason,

        [Parameter(
            ParameterSetName = 'User'
        )]
        [String]
        $MaxBuilds = 10,

        [Switch]
        $NoRetry,

        [Parameter(
            ValueFromPipelineByPropertyName = $true
        )]
        [System.Object[]]
        $Project = $env:SYSTEM_TEAMPROJECT,

        [String]
        $CollectionUri = $env:SYSTEM_COLLECTIONURI,

        [string]
        $Pat = $env:SYSTEM_ACCESSTOKEN
    )

    begin {
        $script:AzApiHeaders = @{
            Headers       = Initialize-AzDORestApi -Pat $Pat
            CollectionUri = $CollectionUri
            ApiVersion    = '6.1'
        }
    }

    process {
        . $PSScriptRoot/private/Get-AzDOApiProjectName.ps1
        $Project = $Project | Get-AzDOApiProjectName

        foreach ($id in $BuildId) {
            foreach ($projectName in $Project) {
                try {
                    $buildInfo = Invoke-AzDORestApiMethod `
                        @script:AzApiHeaders `
                        -Method Get `
                        -Project $projectName `
                        -Endpoint "build/builds/$id" `
                        -NoRetry `
                        -WhatIf:$false `
                        -ErrorAction SilentlyContinue
                }
                catch {
                    Write-Verbose -Message (
                        $_ | Out-String
                    )
                }
                if ($buildInfo) {
                    $buildInfo
                }
                else {
                    Write-Warning -Message "Build $id not found in $projectName."
                }
            }
        }

        foreach ($email in $User) {
            $params = @(
                "requestedFor=$email",
                "`$top=$MaxBuilds",
                'buildQueryOrder=queueTimeDescending'
            )
            if ($Result) {
                $params += "resultFilter=$($Result -join ',')"
            }
            if ($Status) {
                $params += "statusFilter=$($Status -join ',')"
            }
            if ($Reason) {
                $params += "reasonFilter=$($Reason -join ',')"
            }

            foreach ($projectName in $Project) {
                $buildInfo = Invoke-AzDORestApiMethod `
                    @script:AzApiHeaders `
                    -Method Get `
                    -Project $projectName `
                    -Endpoint 'build/builds' `
                    -Params $params `
                    -NoRetry:$NoRetry `
                    -WhatIf:$false

                if ($buildInfo) {
                    $buildInfo
                }
                else {
                    Write-Warning -Message (
                        "$($Result -join '/')/$($Status -join '/')/$($Reason -join '/') " +
                        "builds for $User not found in $projectName."
                    )
                }
            }
        }
    }
}

<#
.SYNOPSIS
Short description
 
.DESCRIPTION
Long description
 
.PARAMETER BuildPipeline
A build pipeline object returned from Get-AzDOPipeline.
 
.PARAMETER Branch
Only return builds from a given branch.
 
.PARAMETER MaxBuilds
The number of most recent builds to get. Defaults to 10.
 
.PARAMETER HistoryInDays
Only return builds from this number of days in the past (including today).
 
.PARAMETER IncludePr
Include PR builds in the list (disabled by default).
 
.PARAMETER HasResult
Only return builds that have failed, succeeded or partially succeeded.
 
.PARAMETER Succeeded
Include only completed and succeeded builds.
 
.PARAMETER Pat
Personal access token authorized to administer builds. Defaults to $env:SYSTEM_ACCESSTOKEN for use in Azure
Pipelines.
 
.EXAMPLE
Get-AzDOPipeline -Name utils-integration-checkin -Project MyProject | Get-AzDOPipelineRunList -MaxBuilds 3
 
.NOTES
The Cmdlet will work as-is in a UI Pipeline with the default $Pat parameter as long as OAUTH access has been
enabled for the pipeline/job. If using a YAML build, the system.accesstoken variable needs to be explicitly
mapped to the steps environment like the following example:
 
steps:
- powershell: Invoke-WebRequest -Uri $Uri -Headers ( Initialize-AzDORestApi )
  env:
    SYSTEM_ACCESSTOKEN: $(system.accesstoken)
 
.LINK
https://docs.microsoft.com/en-us/rest/api/azure/devops/build/builds/list
#>

function Get-AzDOPipelineRunList {
    [CmdletBinding(DefaultParameterSetName = 'Max')]
    param (
        [Parameter(ValueFromPipeline = $true, Mandatory = $true)]
        [System.Object[]]$BuildPipeline,
        [String]$Branch,
        [Parameter(ParameterSetName = 'Max')]
        [Int]$MaxBuilds,
        [Parameter(ParameterSetName = 'Date')]
        [Int]$HistoryInDays,
        [Switch]$IncludePr,
        [Switch]$InProgress,
        [Switch]$HasResult,
        [Alias('Completed')]
        [Switch]$Succeeded,
        [Switch]$NoRetry,
        [String]$CollectionUri = $env:SYSTEM_COLLECTIONURI,
        [String]$Pat = $env:SYSTEM_ACCESSTOKEN
    )

    begin {
        $script:AzApiHeaders = @{
            Headers       = Initialize-AzDORestApi -Pat $Pat
            CollectionUri = $CollectionUri
            ApiVersion    = '6.1'
        }
    }

    process {
        foreach ($pipeline in $BuildPipeline) {
            do {
                $params = @(
                    "definitions=$($pipeline.id -join ',')"
                )

                if ($HistoryInDays) {
                    $minTime = [DateTime]::Today.AddDays(-$HistoryInDays).ToUniversalTime() |
                        Get-Date -Format 'yyyy-MM-ddTHH:mm:ss.ffK'
                    $params += @(
                        "minTime=$minTime"
                    )
                }

                $buildReasons = (
                    'reasonFilter=batchedCI,buildCompletion,resourceTrigger,individualCI,manual,schedule,triggered'
                )
                if ($IncludePr) {
                    $buildReasons += ',pullRequest'
                }
                $params += $buildReasons

                if ($InProgress) {
                    $params += @(
                        'statusFilter=inProgress,NotStarted'
                    )
                }
                elseif ($HasResult) {
                    $params += @(
                        'statusFilter=completed',
                        'resultFilter=canceled,failed,partiallySucceeded,succeeded'
                    )
                }
                elseif ($Succeeded) {
                    $params += @(
                        'statusFilter=completed',
                        'resultFilter=succeeded'
                    )
                }

                if ($MaxBuilds -and !$HistoryInDays) {
                    # Add the amount of erroneous PR builds included in the result and had to be removed
                    if ($null -ne $finalBuildInfo) {
                        $addMore = $top - $finalBuildInfo.Count
                    }
                    else {
                        $addMore = 0
                    }
                    $top = $MaxBuilds + $addMore
                    $params += @(
                        "`$top=$top"
                    )
                }

                if ($Branch) {
                    $params += "branchName=refs/heads/$Branch"
                }


                $buildInfo = @()
                $buildInfo += Invoke-AzDORestApiMethod `
                    @script:AzApiHeaders `
                    -Method Get `
                    -Project $pipeline.project.name `
                    -Endpoint 'build/builds' `
                    -Params $params `
                    -NoRetry:$NoRetry |
                    Sort-Object -Descending -Property id
                if ($buildInfo -and !$IncludePr) {
                    # The reasonFilter parameter seems to be broken so we
                    # need to manually filter pullRequest builds
                    $finalBuildInfo = @()
                    $finalBuildInfo += $buildInfo | Where-Object -Property reason -NE 'pullRequest'
                }
                elseif ($buildInfo) {
                    $finalBuildInfo = $buildInfo
                }
                # Since the pullRequest reason filter isn't working, it's possible that
                # MaxBuilds won't find a single build with the specified parameter, so we
                # need to loop through until it's met.
            } while (
                $buildInfo -and
                $finalBuildInfo.Count -lt $MaxBuilds -and
                ($top -lt 100 -or $top -le ($MaxBuilds * 2))
            )
            if ($finalBuildInfo) {
                $finalBuildInfo
            }
            else {
                Write-Warning -Message (
                    "No builds found in the $($pipeline.name) pipeline with " +
                    "the specified parameters:`n`t" +
                    ($params -join "`n`t")
                )
            }
        }
    }
}

<#
.SYNOPSIS
Gets tags from a build.
 
.DESCRIPTION
Gets tags from a build.
 
.PARAMETER BuildId
ID of the build to get tags from.
 
.PARAMETER NoRetry
Don't retry the API call if it fails.
 
.PARAMETER Project
Project that the build's pipeline resides in.
 
.PARAMETER CollectionUri
The Project Collection URI (https://dev.azure.com/[organization])
 
.PARAMETER Pat
An Azure DevOps Personal Access Token authorized to view pipelines.
 
.EXAMPLE
Get-AzDOPipelineRunTag -BuildId 11111 -Project Tools
Gets all tags from build 11111.
 
.NOTES
General notes
 
.LINK
https://docs.microsoft.com/en-us/rest/api/azure/devops/build/tags/get%20tags
#>

function Get-AzDOPipelineRunTag {
    param (
        [Parameter(Position = 0, ValueFromPipelineByPropertyName = $true)]
        [Alias('Id')]
        [String[]]
        $BuildId = $env:BUILD_BUILDID,
        [Switch]$NoRetry,
        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [System.Object]
        $Project = $env:SYSTEM_TEAMPROJECT,
        [String]
        $CollectionUri = $env:SYSTEM_COLLECTIONURI,
        [String]
        $Pat = $env:SYSTEM_ACCESSTOKEN
    )

    begin {
        $script:AzApiHeaders = @{
            Headers       = Initialize-AzDORestApi -Pat $Pat
            CollectionUri = $CollectionUri
            ApiVersion    = '5.1'
        }
    }

    process {
        . $PSScriptRoot/private/Get-AzDOApiProjectName.ps1
        $Project = $Project | Get-AzDOApiProjectName

        foreach ($id in $BuildId) {
            Invoke-AzDORestApiMethod `
                @script:AzApiHeaders `
                -Method Get `
                -Project $Project `
                -Endpoint "build/builds/$id/tags" `
                -NoRetry:$NoRetry
        }
    }
}

<#
.SYNOPSIS
Gets a release pipeline definition object from Azure Pipelines.
 
.DESCRIPTION
Gets a release pipeline definition object from Azure Pipelines using a project and name filter.
 
.PARAMETER Name
A filter to search for release pipeline names.
 
.PARAMETER Id
The release pipeline ID to get.
 
.PARAMETER Project
Project that the release pipelines reside in.
 
.PARAMETER CollectionUri
The project collection URL (https://dev.azure.com/[orgranization]).
 
.PARAMETER Pat
Personal access token authorized to administer releases. Defaults to $env:SYSTEM_ACCESSTOKEN for use in Azure
Pipelines.
 
.EXAMPLE
Get-AzDOReleasePipeline -Project Packages -Name ReleasePipeline*
 
.LINK
https://learn.microsoft.com/en-us/rest/api/azure/devops/release/definitions/get
 
.LINK
https://docs.microsoft.com/en-us/rest/api/azure/devops/release/pipelines/list
 
.NOTES
The Cmdlet will work as-is in a UI Pipeline with the default $Pat parameter as long as OAUTH access has been
enabled for the pipeline/job. If using a YAML build, the system.accesstoken variable needs to be explicitly
mapped to the steps environment like the following example:
 
steps:
- powershell: Invoke-WebRequest -Uri $Uri -Headers ( Initialize-AzDORestApi )
  env:
    SYSTEM_ACCESSTOKEN: $(system.accesstoken)
#>


function Get-AzDOReleasePipeline {
    [CmdletBinding(DefaultParameterSetName = 'Name')]
    param (
        [Parameter(ParameterSetName = 'Name', Position = 0)]
        [String[]]$Name,
        [Parameter(ParameterSetName = 'Id', Position = 0)]
        [Int[]]$Id,
        [Switch]$NoRetry,
        [String[]]$Project = $env:SYSTEM_TEAMPROJECT,
        [String]$CollectionUri = $env:SYSTEM_COLLECTIONURI,
        [string]$Pat = $env:SYSTEM_ACCESSTOKEN
    )

    begin {
        . "$PSScriptRoot/private/Add-AzDOProject.ps1"

        $script:AzApiHeaders = @{
            Headers       = Initialize-AzDORestApi -Pat $Pat
            CollectionUri = $CollectionUri
            ApiVersion    = '7.2-preview.4'
        }
    }

    process {
        if ($Id) {
            foreach ($projectName in $Project) {
                $pipeline = Invoke-AzDORestApiMethod `
                    @script:AzApiHeaders `
                    -Method Get `
                    -SubDomain vsrm `
                    -Project $projectName `
                    -Endpoint 'release/definitions' `
                    -Params @(
                        "definitionIds=$($Id -join ',')"
                        'propertyFilters=variables,environments'
                     ) `
                    -NoRetry:$NoRetry `
                    -WhatIf:$false
                if ($pipeline) {
                    $pipeline
                }
                else {
                    Write-Warning -Message "Pipeline $Id not found in $projectName."
                }
            }
        }
        elseif ($Name) {
            foreach ($filter in $Name) {
                foreach ($projectName in $Project) {
                    $pipelineResponse = Invoke-AzDORestApiMethod `
                        @script:AzApiHeaders `
                        -Method Get `
                        -SubDomain vsrm `
                        -Project $projectName `
                        -Endpoint 'release/definitions' `
                        -Params @(
                            "searchText=$filter"
                            'propertyFilters=variables,environments'
                         ) `
                        -NoRetry:$NoRetry `
                        -WhatIf:$false
                    if ($pipelineResponse) {
                        $pipelineResponse | Add-AzDOProject -NoRetry:$NoRetry -CollectionUri $CollectionUri -Pat $Pat
                    }
                    else {
                        Write-Warning -Message "No pipelines found matching '$filter' in $projectName."
                    }
                }
            }
        }
        else {
            foreach ($projectName in $Project) {
                $pipelineResponse = Invoke-AzDORestApiMethod `
                    @script:AzApiHeaders `
                    -Method Get `
                    -SubDomain vsrm `
                    -Project $projectName `
                    -Endpoint 'release/definitions' `
                    -Params 'propertyFilters=variables,environments' `
                    -NoRetry:$NoRetry `
                    -WhatIf:$false
                if ($pipelineResponse) {
                    $pipelineResponse | Add-AzDOProject -NoRetry:$NoRetry -CollectionUri $CollectionUri -Pat $Pat
                }
                else {
                    Write-Warning -Message "No pipelines found in $projectName."
                }
            }
        }
    }
}

<#
.SYNOPSIS
Removes a tag from a build.
 
.DESCRIPTION
Removes a tag from a build, multiple tags, or all tags if none are specified.
 
.PARAMETER Tag
Tag(s) to remove from the build.
 
.PARAMETER BuildId
ID of the build to remove tags from.
 
.PARAMETER Project
Project that the build's pipeline resides in.
 
.PARAMETER CollectionUri
The Project Collection URI (https://dev.azure.com/[organization])
 
.PARAMETER Pat
An Azure DevOps Personal Access Token authorized to manage pipelines.
 
.PARAMETER Force
Don't ask for confirmation before removing the tags.
 
.EXAMPLE
Get-AzDOPipelineRun -Id 11111 -Project Tools | Remove-AzDOPipelineRunTag -Tag tag1, tag2
Removes tags tag1 and tag2 from build 11111.
 
.EXAMPLE
Get-AzDOPipelineRun -Id 11111 -Project Tools | Remove-AzDOPipelineRunTag
Removes all tags from build 11111.
 
.NOTES
N/A
 
.LINK
https://docs.microsoft.com/en-us/rest/api/azure/devops/build/tags/delete%20build%20tag
#>

function Remove-AzDOPipelineRunTag {
    [CmdletBinding(SupportsShouldProcess = $true)]
    param (
        [Parameter(Position = 0)]
        [Alias('Tags')]
        [String[]]$Tag,
        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [Alias('Id')]
        [String[]]
        $BuildId = $env:BUILD_BUILDID,
        [Switch]
        $NoRetry,
        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [System.Object]
        $Project = $env:SYSTEM_TEAMPROJECT,
        [String]
        $CollectionUri = $env:SYSTEM_COLLECTIONURI,
        [String]
        $Pat = $env:SYSTEM_ACCESSTOKEN,
        [Switch]
        $Force
    )

    begin {
        $script:AzApiHeaders = @{
            Headers       = Initialize-AzDORestApi -Pat $Pat
            CollectionUri = $CollectionUri
            ApiVersion    = '5.1'
        }
    }

    process {
        . $PSScriptRoot/private/Get-AzDOApiProjectName.ps1
        $Project = $Project | Get-AzDOApiProjectName

        foreach ($id in $buildId) {
            if (!$Tag) {
                $Tag = Get-AzDOPipelineRunTag `
                    -BuildId $id `
                    -Project $Project `
                    -CollectionUri $CollectionUri `
                    -Pat $Pat `
                    -NoRetry:$NoRetry
            }
            foreach ($buildTag in $Tag) {
                if (!$Force) {
                    $ConfirmPreference = 'Low'
                }
                if ($PSCmdlet.ShouldProcess("Build $id", "Remove tag $buildTag")) {
                    Invoke-AzDORestApiMethod `
                        @script:AzApiHeaders `
                        -Method Delete `
                        -Project $Project `
                        -Endpoint "build/builds/$id/tags/$buildTag" `
                        -Body $body `
                        -NoRetry:$NoRetry `
                        -WhatIf:$false `
                        -Confirm:$false
                }
            }
        }
    }
}

<#
.SYNOPSIS
Updates a release pipeline definition.
 
.DESCRIPTION
Updates a release pipeline definition using a provided json file.
 
.PARAMETER PipelineId
ID of the pipeline to update. Accepts values from the pipeline.
 
.PARAMETER Project
Project that the pipelines reside in.
 
.PARAMETER JsonFilePath
FilePath of the release definition json with updated values.
 
.PARAMETER Pat
Personal access token authorized to administer releases. Defaults to $env:SYSTEM_ACCESSTOKEN for use in
AzurePipelines.
 
.EXAMPLE
Get-AzDOReleasePipeline -Name 'MyRelease' -Project 'MyProject' |
    Set-AzDOReleaseRetention -DaysToKeep 30 -ReleasesToKeep 3
 
id name retentionPolicy
-- ---- ---------------
 1 Stage 1 @{daysToKeep=30; releasesToKeep=3; retainBuild=True}
 
.LINK
https://learn.microsoft.com/en-us/rest/api/azure/devops/release/definitions/update
 
.NOTES
The Cmdlet will work as-is in a UI Pipeline with the default $Pat parameter as long as OAUTH access has been
enabled for the pipeline/job. If using a YAML build, the system.accesstoken variable needs to be explicitly
mapped to the steps environment like the following example:
 
steps:
- powershell: Invoke-WebRequest -Uri $Uri -Headers ( Initialize-AzDORestApi )
  env:
    SYSTEM_ACCESSTOKEN: $(system.accesstoken)
#>


function Set-AzDOReleaseRetention {
    [CmdletBinding(SupportsShouldProcess = $true)]
    param (
        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [Alias('id')]
        [String]$PipelineId,
        [String[]]$Environment,
        [Int]$DaysToKeep = 30,
        [Int]$ReleasesToKeep = 3,
        [Switch]$NoRetry,
        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [System.Object]$Project = $env:SYSTEM_TEAMPROJECT,
        [String]$CollectionUri = $env:SYSTEM_COLLECTIONURI,
        [string]$Pat = $env:SYSTEM_ACCESSTOKEN
    )

    begin {
        $script:AzApiHeaders = @{
            Headers       = Initialize-AzDORestApi -Pat $Pat
            CollectionUri = $CollectionUri
            ApiVersion    = '7.2-preview.4'
        }
    }

    process {
        . $PSScriptRoot/private/Get-AzDOApiProjectName.ps1
        $Project = $Project | Get-AzDOApiProjectName

        $releaseDefinition = Get-AzDOReleasePipeline `
            -Id $PipelineId `
            -NoRetry:$NoRetry `
            -Project $Project `
            -CollectionUri $CollectionUri `
            -Pat $Pat

        $exportedDefinitionFile = $releaseDefinition |
            Export-AzDOPipeline `
                -Destination $env:TEMP `
                -NoRetry:$NoRetry `
                -Project $Project `
                -CollectionUri $CollectionUri `
                -Pat $Pat
        $exportedDefinition = $exportedDefinitionFile |
            Get-Content -Raw -Encoding utf8 |
            ConvertFrom-Json -Depth 10
        $exportedDefinitionFile | Remove-Item -Force

        $environmentsToSet = if ($Environment) {
            foreach ($env in $Environment) {
                $exportedDefinition.environments.name | Where-Object -FilterScript { $_ -eq $env }
            }
        }
        else {
            $exportedDefinition.environments.name
        }

        foreach ($env in $environmentsToSet) {
            $exportedDefinition.environments |
                Where-Object -Property name -EQ $env |
                ForEach-Object -Process {
                    $_.retentionPolicy.daysToKeep = $DaysToKeep
                    $_.retentionPolicy.releasesToKeep = $ReleasesToKeep
                    $_.retentionPolicy.retainBuild = $true
                }
        }

        if ($PSCmdlet.ShouldProcess(
            "Pipeline: $PipelineId",
            "Update retention policy to keep $ReleasesToKeep builds and $DaysToKeep days."
        )) {
            Invoke-AzDORestApiMethod `
                @script:AzApiHeaders `
                -Method Put `
                -SubDomain vsrm `
                -Project $Project `
                -Endpoint "release/definitions/$PipelineId" `
                -Body ( $exportedDefinition | ConvertTo-Json -Depth 10 -Compress ) `
                -NoRetry:$NoRetry |
                Select-Object -ExpandProperty environments |
                Where-Object -FilterScript { $environmentsToSet -contains $_.name } |
                Select-Object -Property id, name, retentionPolicy
        }
    }
}

<#
.SYNOPSIS
Queues a build for the specified pipeline.
 
.DESCRIPTION
Queues a build for the specified pipeline.
 
.PARAMETER BuildPipeline
A build pipeline object returned from Get-AzDOPipeline.
 
.PARAMETER Branch
The branch to queue the build for.
 
.PARAMETER Parameter
A hash table of queue-time parameters to use when starting the build.
 
.PARAMETER CollectionUri
The project collection URL (https://dev.azure.com/[orgranization]).
 
.PARAMETER Pat
Personal access token authorized to administer builds. Defaults to $env:SYSTEM_ACCESSTOKEN for use in
AzurePipelines.
 
.LINK
https://docs.microsoft.com/en-us/rest/api/azure/devops/pipelines/runs/run-pipeline
 
.EXAMPLE
Get-AzDOPipeline -Name utils-integration-checkin | Start-AzDOPipelineRun
 
.EXAMPLE
Get-AzDOPipeline -Name utils-int-karl-checkin | Start-AzDOPipelineRun -Parameter 'buildConfiguration:Release '
 
.Example
Get-AzDOPipeline -Name MxProduct-int-karl-checkin -Project MyProject |
    Start-AzDOPipelineRun -Branch int-karl -Parameter @{ buildConfiguration = 'Release ' }
 
.NOTES
The Cmdlet will work as-is in a UI Pipeline with the default $Pat parameter as long as OAUTH access has been
enabled for the pipeline/job. If using a YAML build, the system.accesstoken variable needs to be explicitly
mapped to the steps environment like the following example:
 
steps:
- powershell: Invoke-WebRequest -Uri $Uri -Headers ( Initialize-AzDORestApi )
  env:
    SYSTEM_ACCESSTOKEN: $(system.accesstoken)
#>


function Start-AzDOPipelineRun {
    [CmdletBinding(SupportsShouldProcess = $true)]
    param (
        [Parameter(ValueFromPipeline = $true, Mandatory = $true)]
        [System.Object[]]$BuildPipeline,
        [String[]]$Branch,
        [System.Object]$Parameter,
        [Switch]$NoRetry,
        [String]$CollectionUri = $env:SYSTEM_COLLECTIONURI,
        [String]$Pat = $env:SYSTEM_ACCESSTOKEN
    )

    begin {
        $script:AzApiHeaders = @{
            Headers       = Initialize-AzDORestApi -Pat $Pat
            CollectionUri = $CollectionUri
            ApiVersion    = '6.1'
        }
    }

    process {
        foreach ($pipeline in $BuildPipeline) {
            $latestBuild = $pipeline | Get-AzDOPipelineRunList `
                -MaxBuilds 1 `
                -Succeeded `
                -CollectionUri $CollectionUri `
                -Pat $Pat `
                -NoRetry:$NoRetry
            if (!$latestBuild) {
                $latestBuild = $pipeline | Get-AzDOPipelineRunList `
                    -MaxBuilds 1 `
                    -IncludePr `
                    -CollectionUri $CollectionUri `
                    -Pat $Pat `
                    -NoRetry:$NoRetry
            }
            if (!$Branch) {
                $Branch = @(
                    Get-AzDORepository `
                        -Name $latestBuild.repository.name `
                        -NoRetry:$NoRetry `
                        -Project $latestBuild.project.name `
                        -CollectionUri $CollectionUri `
                        -Pat $Pat |
                        Select-Object -ExpandProperty 'defaultBranch' |
                        ForEach-Object -Process {
                            $_.Replace('refs/heads/', '')
                        }
                )
            }
            foreach ($ref in $Branch) {
                $body = @{
                    resources          = @{
                        repositories = @{
                            self = @{
                                refName = "refs/heads/$ref"
                            }
                        }
                    }
                    templateParameters = $Parameter
                } | ConvertTo-Json -Depth 6 -Compress
                if ($PSCmdlet.ShouldProcess("$($pipeline.name)", "Queue $ref branch")) {
                    $newRun = Invoke-AzDORestApiMethod `
                        @script:AzApiHeaders `
                        -Method 'Post' `
                        -Project "$($pipeline.project.name)" `
                        -Endpoint "pipelines/$($pipeline.id)/runs" `
                        -Body $body `
                        -NoRetry:$NoRetry
                    if ($env:BUILD_BUILDID) {
                        $tag = "Started via AzDOCmd by $env:BUILD_DEFINITIONNAME - $env:BUILD_BUILDID"
                    }
                    elseif ($env:BUILD_REQUESTEDFOREMAIL) {
                        $tag = "Started via AzDOCmd by $env:BUILD_REQUESTEDFOREMAIL"
                    }
                    else {
                        $tag = 'Started via AzDOCmd'
                    }
                    $newBuild = Get-AzDOPipelineRun `
                        -BuildId $newRun.id `
                        -Project $pipeline.project.name `
                        -CollectionUri $CollectionUri `
                        -Pat $Pat `
                        -NoRetry:$NoRetry
                    $null = $newBuild | Add-AzDOPipelineRunTag `
                        -Tag $tag `
                        -CollectionUri $CollectionUri `
                        -Pat $Pat `
                        -NoRetry:$NoRetry
                    $newBuild | Get-AzDOPipelineRun `
                        -CollectionUri $CollectionUri `
                        -Pat $Pat `
                        -NoRetry:$NoRetry
                }
            }
        }
    }
}

<#
.SYNOPSIS
Stops a build.
 
.DESCRIPTION
Stops a build by ID or object returned from another AzDOCmd build cmdlet.
 
.PARAMETER BuildId
The ID of the build to get.
 
.PARAMETER Project
Project that the build's pipeline resides in.
 
.PARAMETER Pat
Personal access token authorized to administer builds. Defaults to $env:SYSTEM_ACCESSTOKEN for use in Azure
Pipelines.
 
.PARAMETER PassThru
Outputs the cancelled build is passed.
 
.EXAMPLE
Get-AzDOPipeline -Name PipelineTest | Get-AzDOPipelineRunList -MaxBuilds 1 | Stop-AzDOPipelineRun
Stops the latest build in the PipelineTest build pipeline.
 
.LINK
https://docs.microsoft.com/en-us/rest/api/azure/devops/build/builds/update%20build
 
.NOTES
The Cmdlet will work as-is in a UI Pipeline with the default $Pat parameter as long as OAUTH access has been
enabled for the pipeline/job. If using a YAML build, the system.accesstoken variable needs to be explicitly
mapped to the steps environment like the following example:
 
steps:
- powershell: Invoke-WebRequest -Uri $Uri -Headers ( Initialize-AzDORestApi )
  env:
    SYSTEM_ACCESSTOKEN: $(system.accesstoken)
#>


function Stop-AzDOPipelineRun {
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')]
    [CmdletBinding(SupportsShouldProcess = $true)]
    param (
        [Parameter(ValueFromPipelineByPropertyName = $true, Mandatory = $true)]
        [Alias('id')]
        [String[]]$BuildId,
        [Switch]$NoRetry,
        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [System.Object]$Project = $env:SYSTEM_TEAMPROJECT,
        [String]$CollectionUri = $env:SYSTEM_COLLECTIONURI,
        [String]$Pat = $env:SYSTEM_ACCESSTOKEN,
        [Switch]$PassThru
    )

    begin {
        $script:AzApiHeaders = @{
            Headers       = Initialize-AzDORestApi -Pat $Pat
            CollectionUri = $CollectionUri
            ApiVersion    = '6.1'
        }
    }

    process {
        . $PSScriptRoot/private/Get-AzDOApiProjectName.ps1
        $Project = $Project | Get-AzDOApiProjectName

        foreach ($id in $BuildId) {
            $action = @{
                status = 'Cancelling'
            }
            $json = $action | ConvertTo-Json
            Write-Verbose -Message 'Payload:'
            Write-Verbose -Message $json
            $removedBuild = Invoke-AzDORestApiMethod `
                @script:AzApiHeaders `
                -Method Patch `
                -Project $Project `
                -Endpoint "build/builds/$id" `
                -Body $json `
                -NoRetry:$NoRetry
            Write-Verbose -Message 'The following build was cancelled:'
            Write-Verbose -Message ( $removedBuild | Format-Table | Out-String )
            if ($env:BUILD_BUILDID) {
                $tag = "Stopped via AzDOCmd by $env:BUILD_DEFINITIONNAME - $env:BUILD_BUILDID"
            }
            elseif ($env:BUILD_REQUESTEDFOREMAIL) {
                $tag = "Stopped via AzDOCmd by $env:BUILD_REQUESTEDFOREMAIL"
            }
            else {
                $tag = 'Stopped via AzDOCmd'
            }
            $null = $removedBuild | Add-AzDOPipelineRunTag `
                -Tag $tag `
                -CollectionUri $CollectionUri `
                -Pat $Pat `
                -NoRetry:$NoRetry
            if ($PassThru) {
                $removedBuild | Get-AzDOPipelineRun -CollectionUri $CollectionUri -Pat $Pat -NoRetry:$NoRetry
            }
        }
    }
}

<#
.SYNOPSIS
Updates a build pipeline definition.
 
.DESCRIPTION
Updates a build pipeline definition using a provided json file.
 
.PARAMETER PipelineId
ID of the pipeline to update. Accepts values from the pipeline.
 
.PARAMETER Project
Project that the pipelines reside in.
 
.PARAMETER JsonFilePath
FilePath of the build definition json with updated values.
 
.PARAMETER Pat
Personal access token authorized to administer builds. Defaults to $env:SYSTEM_ACCESSTOKEN for use in
AzurePipelines.
 
.EXAMPLE
Update-AzDOPipeline -PipelineId 5992 -Project Packages -JsonFilePath ./azure-pipelines/AzurePipelines-CI.json
 
.NOTES
In order to update a build definition, the `"processParameters": {}` attribute must be included.
 
The Cmdlet will work as-is in a UI Pipeline with the default $Pat parameter as long as OAUTH access has been
enabled for the pipeline/job. If using a YAML build, the system.accesstoken variable needs to be explicitly
mapped to the steps environment like the following example:
 
steps:
- powershell: Invoke-WebRequest -Uri $Uri -Headers ( Initialize-AzDORestApi )
  env:
    SYSTEM_ACCESSTOKEN: $(system.accesstoken)
#>


function Update-AzDOPipeline {
    [CmdletBinding(SupportsShouldProcess = $true)]
    param (
        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [Alias('id')]
        [String]$PipelineId,
        [String]$JsonFilePath,
        [Switch]$NoRetry,
        [String[]]$Project = $env:SYSTEM_TEAMPROJECT,
        [String]$CollectionUri = $env:SYSTEM_COLLECTIONURI,
        [string]$Pat = $env:SYSTEM_ACCESSTOKEN
    )

    begin {
        $script:AzApiHeaders = @{
            Headers       = Initialize-AzDORestApi -Pat $Pat
            CollectionUri = $CollectionUri
            ApiVersion    = '6.1'
        }
    }

    process {
        . $PSScriptRoot/private/Get-AzDOApiProjectName.ps1
        $Project = $Project | Get-AzDOApiProjectName

        if ($PSCmdlet.ShouldProcess(
            "Pipeline: $PipelineId",
            "Update pipeline in project $Project with values from $JsonFilePath."
        )) {
            Invoke-AzDORestApiMethod `
                @script:AzApiHeaders `
                -Method Put `
                -Project $Project `
                -Endpoint "build/definitions/$PipelineId" `
                -Body ( Get-Content -Path $JsonFilePath -Encoding UTF8 | Out-String ) `
                -NoRetry:$NoRetry
        }
    }
}

<#
.SYNOPSIS
Updates a release pipeline definition.
 
.DESCRIPTION
Updates a release pipeline definition using a provided json file.
 
.PARAMETER PipelineId
ID of the pipeline to update. Accepts values from the pipeline.
 
.PARAMETER Project
Project that the pipelines reside in.
 
.PARAMETER JsonFilePath
FilePath of the release definition json with updated values.
 
.PARAMETER Pat
Personal access token authorized to administer releases. Defaults to $env:SYSTEM_ACCESSTOKEN for use in
AzurePipelines.
 
.EXAMPLE
Update-AzDOReleasePipeline -PipelineId 5992 -Project Packages -JsonFilePath ./azure-pipelines/AzurePipelines-CI.json
 
.LINK
https://learn.microsoft.com/en-us/rest/api/azure/devops/release/definitions/update
 
.NOTES
The Cmdlet will work as-is in a UI Pipeline with the default $Pat parameter as long as OAUTH access has been
enabled for the pipeline/job. If using a YAML build, the system.accesstoken variable needs to be explicitly
mapped to the steps environment like the following example:
 
steps:
- powershell: Invoke-WebRequest -Uri $Uri -Headers ( Initialize-AzDORestApi )
  env:
    SYSTEM_ACCESSTOKEN: $(system.accesstoken)
#>


function Update-AzDOReleasePipeline {
    [CmdletBinding(SupportsShouldProcess = $true)]
    param (
        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [Alias('id')]
        [String]$PipelineId,
        [String]$JsonFilePath,
        [Switch]$NoRetry,
        [String[]]$Project = $env:SYSTEM_TEAMPROJECT,
        [String]$CollectionUri = $env:SYSTEM_COLLECTIONURI,
        [string]$Pat = $env:SYSTEM_ACCESSTOKEN
    )

    begin {
        $script:AzApiHeaders = @{
            Headers       = Initialize-AzDORestApi -Pat $Pat
            CollectionUri = $CollectionUri
            ApiVersion    = '7.2-preview.4'
        }
    }

    process {
        . $PSScriptRoot/private/Get-AzDOApiProjectName.ps1
        $Project = $Project | Get-AzDOApiProjectName

        if ($PSCmdlet.ShouldProcess(
            "Release pipeline: $PipelineId",
            "Update release pipeline in project $Project with values from $JsonFilePath."
        )) {
            Invoke-AzDORestApiMethod `
                @script:AzApiHeaders `
                -Method Put `
                -Project $Project `
                -Endpoint "release/definitions/$PipelineId" `
                -Body ( Get-Content -Path $JsonFilePath -Encoding UTF8 | Out-String ) `
                -NoRetry:$NoRetry
        }
    }
}

<#
.SYNOPSIS
Reports build status until completion.
 
.DESCRIPTION
Reports build status until completion, either by build ID or user email.
 
.PARAMETER BuildId
ID of the pipeline runs to watch.
 
.PARAMETER User
Email address of the user the pipeline runs are requested for.
 
.PARAMETER Project
The Azure DevOps project(s) where the pipeline runs are running. Defaults to $env:SYSTEM_TEAMPROJECT to work with Azure
Pipelines.
 
.PARAMETER PollingInterval
Time period in seconds in between getting build information. Defaults to 15 seconds.
 
.PARAMETER Pat
An Azure DevOps Personal Access Token authorized to read pipeline runs.
 
.EXAMPLE
Watch-UserBuild -User myorg@dev.azure.com -Project MyProject
 
.EXAMPLE
Get-AzDOPipeline -Name 'PipelineTest-Short' -Project Tools |
    Start-AzDOPipelineRun -Branch main |
    Watch-AzDOPipelineRun
 
.NOTES
N/A
#>


function Watch-AzDOPipelineRun {
    [CmdletBinding(DefaultParameterSetName = 'User')]
    param (
        [Parameter(ParameterSetName = 'BuildId', Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [Alias('id')]
        [Int[]]$BuildId,
        [Parameter(ParameterSetName = 'User')]
        [String[]]$User = @($env:BUILD_REQUESTEDFOREMAIL),
        [String]$PollingInterval = 15,
        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [System.Object[]]$Project = $env:SYSTEM_TEAMPROJECT,
        [String]$CollectionUri = $env:SYSTEM_COLLECTIONURI,
        [String]$Pat = $env:SYSTEM_ACCESSTOKEN
    )

    begin {
        <#
        .SYNOPSIS
        Convert raw JSON into PowerShell readable object.
        #>

        function ConvertTo-PipelineRunObject {
            param (
                [System.Object]$JsonObject
            )
            $userBuild = [PSCustomObject]@{
                BuildId      = $JsonObject.id
                PipelineName = $JsonObject.definition.name
                Project      = $JsonObject.project.name
                Status       = $JsonObject.status
                Result       = $JsonObject.result
                BuildName    = $JsonObject.buildNumber
                Reason       = $JsonObject.reason
                Tags         = ($JsonObject.tags -join ', ')
                Url          = $JsonObject._links.web.href
            }
            # Set default output to table. From:
            # https://learn-powershell.net/2013/08/03
            # /quick-hits-set-the-default-property-display-in-powershell-on-custom-objects/
            $userBuild.PSObject.TypeNames.Insert(0, 'Build')
            $defaultDisplaySet = 'BuildId', 'PipelineName', 'Status', 'Result'
            $defaultDisplayPropertySet = New-Object `
                -TypeName System.Management.Automation.PSPropertySet(
                'DefaultDisplayPropertySet', [string[]]$defaultDisplaySet
            )
            $PSStandardMembers = (
                [System.Management.Automation.PSMemberInfo[]]@($defaultDisplayPropertySet)
            )
            $userBuild |
                Add-Member `
                    -MemberType MemberSet `
                    -Name PSStandardMembers `
                    -Value $PSStandardMembers
            $userBuild
        }
    }

    process {
        . $PSScriptRoot/private/Get-AzDOApiProjectName.ps1
        $Project = $Project | Get-AzDOApiProjectName

        $allBuildIds = @()
        $activity = 'Monitoring pipeline runs '
        $activity += if ($BuildId) {
            $BuildId -join ', '
        }
        else {
            "for $($User -join ', ')"
        }
        $activity += " in $($Project -join ', ')"
        $writeProgressArgs = @{
            Activity         = $activity
            CurrentOperation = (
                'Run watch will end when all current pipeline runs are completed. Press Ctrl + C to exit.'
            )
        }
        $firstRun = $true
        $finalRun = $null
        do {
            if ($firstRun) {
                $seconds = 0
            }
            else {
                $seconds = $PollingInterval
            }
            $writeProgressArgs['Status'] = "Waiting $seconds seconds to poll pipeline runs..."
            Write-Progress @writeProgressArgs
            for ($i = 1; $i -le $seconds; $i++) {
                Start-Sleep -Seconds 1
                Write-Progress @writeProgressArgs -PercentComplete (($i / $seconds) * 100)
            }
            Write-Progress @writeProgressArgs -Completed

            $builds = @()
            $writeProgressArgs['Status'] = 'Finding pipeline runs...'
            Write-Progress @writeProgressArgs
            $getBuild = @{
                Project       = $Project
                CollectionUri = $CollectionUri
                Pat           = $Pat
                WarningAction = 'SilentlyContinue'
            }
            $builds += if ($BuildId) {
                Get-AzDOPipelineRun -BuildId $BuildId @getBuild
            }
            else {
                Get-AzDOPipelineRun -User $User -Status @('inProgress', 'notStarted') @getBuild
            }

            $writeProgressArgs['Status'] = 'Getting all queued pipeline runs...'
            Write-Progress @writeProgressArgs
            $allBuildIds += foreach ($id in $builds.id) {
                $id
            }
            $allBuildIds = @( $allBuildIds | Sort-Object -Unique -Descending )
            if ($allBuildIds) {
                do {
                    $allBuilds = Get-AzDOPipelineRun -BuildId $allBuildIds @getBuild
                } while (!$allBuilds)
                $displayBuilds = @()
                $displayBuilds += foreach ($build in $allBuilds) {
                    if ($build.status -ne 'completed') {
                        ConvertTo-PipelineRunObject -JsonObject $build
                    }
                }
                $displayBuilds += foreach ($build in $allBuilds) {
                    if ($build.status -eq 'completed') {
                        ConvertTo-PipelineRunObject -JsonObject $build
                    }
                }
                Write-Progress @writeProgressArgs -Completed
                Clear-Host
                $inProgress = $allBuilds | Where-Object -Property status -Match '(inProgress|notStarted)'

                # Sometimes triggered builds take a few seconds to start,
                # so we need to double check if no builds are found and it isn't
                # the first run.
                if ($finalRun -eq $true) {
                    $finalRun = $false
                }
                if ($inProgress) {
                    $finalRun = $null
                    if (( Get-PSVersion ).Major -lt 7) {
                        Write-Host -Object "`n`n`n`n`n`n`n`n`n"
                    }
                }
                elseif (!$firstRun -and $finalRun -ne $false) {
                    $finalRun = $true
                }
                $firstRun = $false

                $consoleHeight = $Host.UI.RawUI.WindowSize.Height
                $buildsToDisplay = [Math]::max(0, $consoleHeight - 13)
                Write-Host -Object (
                    $displayBuilds |
                        Select-Object -Property * -First $buildsToDisplay |
                        Format-Table |
                        Out-String
                ).Trim()
                if ($displayBuilds.Count -ge $buildsToDisplay) {
                    Write-Host `
                        -Object " ......`tincrease the console height to view more pipeline runs." `
                        -ForegroundColor Gray
                }
            }
            Write-Verbose -Message '#############################################################################'
            Write-Verbose -Message (
                "In progress: $( ConvertTo-PipelineRunObject -JsonObject $inProgress -ErrorAction SilentlyContinue )"
            )
            Write-Verbose -Message "Final run: $finalRun"
            Write-Verbose -Message "Should loop again: $(($inProgress -or $finalRun))"
            Write-Verbose -Message '#############################################################################'
        } while ($inProgress -or $finalRun)
        $displayBuilds
    }
}

<#
.SYNOPSIS
Gets file contents from a single file in a git repo.
 
.DESCRIPTION
Gets file contents from a single file in a git repo.
 
.PARAMETER Repository
Repo that the file is in.
 
.PARAMETER Branch
The branch to pull the file from.
 
.PARAMETER Path
Path from the source of the repository. Use `/` to divide folders, and no leading slash.
 
.PARAMETER OutFile
Saves the file contents to a file if specified.
 
.PARAMETER Project
Project that the file's repo resides in.
 
.PARAMETER CollectionUri
The project collection URL (https://dev.azure.com/[organization]).
 
.PARAMETER Pat
Personal access token authorized to read code. Defaults to $env:SYSTEM_ACCESSTOKEN for use in
AzurePipelines.
 
.EXAMPLE
Get-AzDOGitItem -OutFile 'README.md'
 
.LINK
https://docs.microsoft.com/en-us/rest/api/azure/devops/git/items/get
 
.NOTES
The Cmdlet will work as-is in a UI Pipeline with the default $Pat parameter as long as OAUTH access has been
enabled for the pipeline/job. If using a YAML build, the system.accesstoken variable needs to be explicitly
mapped to the steps environment like the following example:
 
steps:
- powershell: Invoke-WebRequest -Uri $Uri -Headers ( Initialize-AzDORestApi )
  env:
    SYSTEM_ACCESSTOKEN: $(system.accesstoken)
#>


function Get-AzDOGitItem {
    [CmdletBinding()]
    param (
        [Alias('Repo')]
        [String]$Repository = $env:BUILD_REPOSITORY_NAME,
        [string]$Branch = 'main',
        [string]$Path = 'README.md',
        [string]$OutFile,
        [Switch]$NoRetry,
        [String]$Project = $env:SYSTEM_TEAMPROJECT,
        [String]$CollectionUri = $env:SYSTEM_COLLECTIONURI,
        [String]$Pat = $env:SYSTEM_ACCESSTOKEN
    )

    Invoke-AzDORestApiMethod `
        -Method Get `
        -CollectionUri $CollectionUri `
        -Project $Project `
        -Endpoint "git/repositories/$Repository/items" `
        -Params (
            "scopePath=/$Path",
            'download=true',
            "version=$Branch",
            'versionOptions=None',
            'versionType=branch'
        ) `
        -OutFile:$OutFile `
        -Headers ( Initialize-AzDORestApi -Pat $Pat ) `
        -ApiVersion '7.1-preview.1' `
        -NoRetry:$NoRetry `
        -WhatIf:$WhatIfPreference
}

<#
.SYNOPSIS
Gets info for an Azure Repos repository.
 
.DESCRIPTION
Gets info for an Azure Repos repository.
 
.PARAMETER Name
Name of the repo.
 
.PARAMETER Project
Project that the repo resides in.
 
.PARAMETER CollectionUri
The project collection URL (https://dev.azure.com/[orgranization]).
 
.PARAMETER Pat
An Azure DevOps Personal Access Token authorized to read code.
 
.EXAMPLE
Get-AzDORepository -Name AzDO -Project MyProject
 
.LINK
https://docs.microsoft.com/en-us/rest/api/azure/devops/git/repositories/get%20repository
 
.NOTES
N/A
#>

function Get-AzDORepository {
    [CmdletBinding()]
    param (
        [String]$Name,
        [Switch]$NoRetry,
        [String]$Project = $env:SYSTEM_TEAMPROJECT,
        [String]$CollectionUri = $env:SYSTEM_COLLECTIONURI,
        [String]$Pat = $env:SYSTEM_ACCESSTOKEN
    )

    begin {
        $script:AzApiHeaders = @{
            Headers       = Initialize-AzDORestApi -Pat $Pat
            CollectionUri = $CollectionUri
            ApiVersion    = '7.1-preview.1'
        }
    }

    process {
        Invoke-AzDORestApiMethod `
            @script:AzApiHeaders `
            -Method Get `
            -Project $Project `
            -Endpoint "git/repositories/$Name" `
            -NoRetry:$NoRetry
    }
}

<#
.SYNOPSIS
Initializes environment variables needed to connect to Azure DevOps.
 
.DESCRIPTION
This function initializes environment variables needed to connect to Azure DevOps. If an existing connection is
found, the user is prompted to overwrite the existing connection.
 
.PARAMETER Project
The default Azure DevOps project to use.
 
.PARAMETER CollectionUri
The Azure DevOps project collection URI.
 
.PARAMETER Pat
The Azure DevOps Personal Access Token (PAT) to use.
 
.EXAMPLE
Connect-AzDO
 
.NOTES
N/A
#>

function Connect-AzDO {
    param (
        [String]$Project,
        [String]$CollectionUri,
        [String]$Pat
    )

    $currentAzDOConnection = Get-AzDOConnection
    if ($null -ne ( $currentAzDOConnection.PSObject.Properties.Value | Where-Object -FilterScript {$_} )) {
        Write-Warning -Message 'An existing Azure DevOps connection was found.'
        Write-Information -MessageData ( $currentAzDOConnection | Format-List | Out-String ) -InformationAction Continue
        $response = Read-Host -Prompt 'Would you like to overwrite the existing connection? (y/n)'
        if ($response.ToLower() -ne 'y') {
            return
        }
    }

    while (!$newCollectionUri) {
        $newCollectionUri = if ($CollectionUri) {
            $CollectionUri
        }
        else {
            Read-Host -Prompt (
                "`nPlease enter a Project Collection URI. e.g. " +
                'https://dev.azure.com/[Organization]/'
            )
        }
    }
    Set-EnvironmentVariable -Name 'SYSTEM_COLLECTIONURI' -Value $newCollectionUri -Scope User -Force

    while (!$newProject) {
        $newProject = if ($Project) {
            $Project
        }
        else {
            Read-Host -Prompt "`nPlease enter a default Azure DevOps project"
        }
    }
    Set-EnvironmentVariable -Name 'SYSTEM_TEAMPROJECT' -Value $newProject -Scope User -Force

    while (!$newPat) {
        $newPat = if ($Pat) {
            $Pat
        }
        else {
            Read-Host -Prompt (
                "`n" + 'Please enter an Azure DevOps Personal Access Token (PAT) authorized to access ' +
                'Azure DevOps artifacts. Instructions can be found at:' + "`n`n`t" +
                'https://docs.microsoft.com/en-us/azure/devops/organizations/' +
                'accounts/use-personal-access-tokens-to-authenticate' + "`n`n" +
                'Personal Access Token (PAT)'
            )
        }
    }
    Set-EnvironmentVariable -Name 'SYSTEM_ACCESSTOKEN' -Value $newPat -Scope User -Force

    $currentAzDOConnection = Get-AzDOConnection
    $currentAzDOConnection | Format-List
    $currentAzDOConnection | Test-AzDOConnection
}

<#
.SYNOPSIS
Gets the environment variables being used to connect to Azure DevOps.
 
.DESCRIPTION
Gets the environment variables being used to connect to Azure DevOps.
 
.EXAMPLE
Get-AzDOConnection
 
.NOTES
N/A
#>

function Get-AzDOConnection {
    [CmdletBinding()]
    param ()

    [PSCustomObject]@{
        CollectionURI = $env:SYSTEM_COLLECTIONURI
        Project       = $env:SYSTEM_TEAMPROJECT
        Pat           = $env:SYSTEM_ACCESSTOKEN
    }
}

<#
.SYNOPSIS
Creates authorization headers for an Azure DevOps REST API call.
 
.DESCRIPTION
Creates authorization headers for an Azure DevOps REST API call.
 
.PARAMETER User
Deprecated. Not used for API calls.
 
.PARAMETER Pat
Personal access token authorized for the call being made. Defaults to $env:SYSTEM_ACCESSTOKEN for use in Azure
Pipelines.
 
.PARAMETER Authentication
Choose Basic or Bearer authentication. Note that Bearer authentication will disregard Pat and CollectionUri and
use the current Azure context returned from Get-AzContext.
 
.EXAMPLE
$headers = Initialize-AzDORestApi
 
.LINK
https://learn.microsoft.com/en-us/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate?toc=%2Fazure%2Fdevops%2Fmarketplace-extensibility%2Ftoc.json&view=azure-devops&tabs=Windows#use-a-pat
 
.LINK
https://dotnetdevlife.wordpress.com/2020/02/19/get-bearer-token-from-azure-powershell/
 
.NOTES
The Cmdlet will work as-is in a UI Pipeline with the default $Pat parameter as long as OAUTH access has been
enabled for the pipeline/job. If using a YAML build, the system.accesstoken variable needs to be explicitly
mapped to the steps environment like the following example:
 
steps:
- powershell: Invoke-WebRequest -Uri $Uri -Headers ( Initialize-AzDORestApi )
  env:
    SYSTEM_ACCESSTOKEN: $(system.accesstoken)
#>


function Initialize-AzDORestApi {
    [CmdletBinding()]
    [OutputType([System.Collections.Hashtable])]
    param (
        [String]$User,
        [String]$Pat = $env:SYSTEM_ACCESSTOKEN,
        [ValidateSet('Basic', 'Bearer')]
        [String]$Authentication = 'Basic'
    )

    if ($User) {
        Write-Verbose -Message 'A User was specified but is not needed and will not be used.'
    }

    if ($Authentication -eq 'Basic') {
        $base64EncodedToken = $(
            [Convert]::ToBase64String(
                [Text.Encoding]::ASCII.GetBytes(":$Pat")
            )
        )

        @{
            Authorization = "Basic $base64EncodedToken"
        }
    }
    else {
        try {
            $tenantId = ( Get-AzContext -ErrorAction Stop ).Subscription.TenantId
        }
        catch {
            Connect-AzAccount
            $tenantId = ( Get-AzContext -ErrorAction Stop ).Subscription.TenantId
        }

        $azureRmProfile = [Microsoft.Azure.Commands.Common.Authentication.Abstractions.AzureRmProfileProvider]::Instance.Profile
        Write-Verbose -Message 'Current Azure Context:'
        Write-Verbose -Message $azureRmProfile.DefaultContextKey.ToString()
        Write-Verbose -Message (
            $azureRmProfile.Contexts.Keys | Where-Object -FilterScript { $_ -notmatch 'Concierge' } | Out-String
        )

        $profileClient = New-Object Microsoft.Azure.Commands.ResourceManager.Common.RMProfileClient($azureRmProfile)
        $token = $profileClient.AcquireAccessToken($tenantId).AccessToken
        if ($token) {
            Write-Verbose -Message 'Azure AD bearer token generated!'
        }
        else {
            Write-Error -Message 'Azure AD bearer token unable to be generated.'
        }

        @{
            Accept        = 'application/json'
            Authorization = "Bearer $token"
        }
    }
}

<#
.SYNOPSIS
A wrapper to invoke Azure DevOps API calls.
 
.DESCRIPTION
A wrapper to invoke Azure DevOps API calls. Authorization is provided by Initialize-AzDORestApi.
 
.PARAMETER Method
REST method. Supports GET, PATCH, DELETE, PUT, and POST right now.
 
.PARAMETER CollectionUri
The full Azure DevOps URL of an organization. Can be automatically populated in a pipeline.
 
.PARAMETER Organization
Azure DevOps organization. Used in place of CollectionUri.
 
.PARAMETER SubDomain
Subdomain prefix of dev.azure.com that the API requires.
 
.PARAMETER Project
The project the call will target. Can be automatically populated in a pipeline.
 
.PARAMETER Endpoint
Everything in between the base URI of the rest call and the parameters.
e.g. VERB https://dev.azure.com/{organization}/{team-project}/_apis/{endpoint}?api-version={version}
 
.PARAMETER Params
An array of parameter declarations.
 
.PARAMETER Body
The body of the call if needed.
 
.PARAMETER OutFile
Path to download the output of the rest call.
 
.PARAMETER NoRetry
Don't retry failed calls.
 
.PARAMETER ApiVersion
The version of the API to use.
 
.EXAMPLE
Invoke-AzDORestApiMethod `
    -Method Get `
    -Organization MyOrg `
    -Endpoint 'work/accountmyworkrecentactivity' `
    -Headers ( Initialize-AzDORestApi -Pat $Pat ) `
    -ApiVersion '5.1'
# GET https://dev.azure.com/MyOrg/_apis/work/accountmyworkrecentactivity?api-version=5.1-preview.2
 
.NOTES
The Cmdlet will work as-is in a UI Pipeline with the default $Pat parameter as long as OAUTH access has been
enabled for the pipeline/job. If using a YAML build, the system.accesstoken variable needs to be explicitly
mapped to the steps environment like the following example:
 
steps:
- powershell: Invoke-WebRequest -Uri $Uri -Headers ( Initialize-AzDORestApi )
  env:
    SYSTEM_ACCESSTOKEN: $(system.accesstoken)
#>


function Invoke-AzDORestApiMethod {
    [CmdletBinding(DefaultParameterSetName = 'Uri', SupportsShouldProcess = $true)]
    param (
        [ValidateSet('Get', 'Patch', 'Delete', 'Put', 'Post')]
        [Parameter(Mandatory = $true)]
        [string]$Method,
        [Parameter(ParameterSetName = 'Uri')]
        [string]$CollectionUri = $env:SYSTEM_COLLECTIONURI,
        [Parameter(ParameterSetName = 'Org', Mandatory = $true)]
        [string]$Organization,
        [string]$SubDomain,
        [string]$Project, # = $env:SYSTEM_TEAMPROJECT
        [Parameter(Mandatory = $true)]
        [string]$Endpoint,
        [string[]]$Params,
        [string]$Body,
        [string]$OutFile,
        [Switch]$NoRetry,
        [string]$ApiVersion = '6.0',
        [hashtable]$Headers = ( Initialize-AzDORestApi )
    )

    $cachedProgressPreference = $ProgressPreference

    if ($PSCmdlet.ParameterSetName -eq 'Org') {
        $CollectionUri = "https://dev.azure.com/$Organization/"
    }
    else {
        $Organization = $CollectionUri.`
            Replace('https://', '').`
            Replace('dev.azure.com', '').`
            Replace('.visualstudio.com', '').`
            Replace('/', '')
    }
    if ($CollectionUri -match '.*\.visualstudio\.com') {
        $CollectionUri = "https://dev.azure.com/$Organization/"
    }
    if ($SubDomain) {
        if ($SubDomain -eq 'azdevopscommerce') {
            $CollectionUri = $CollectionUri.Replace(
                $Organization,
                ( Get-AzDoOrganizationId -CollectionUri $CollectionUri )
            )
        }
        $CollectionUri = $CollectionUri.Replace('dev.azure.com', "$SubDomain.dev.azure.com")
    }
    if ($CollectionUri -notmatch '/$') {
        $CollectionUri += '/'
    }
    $restUri = $CollectionUri

    if (![String]::isNullOrEmpty($Project)) {
        $restUri += [Uri]::EscapeDataString($Project) + '/'
    }

    if ($Params.Length -eq 0) {
        $paramString = "api-version=$ApiVersion"
    }
    else {
        $paramString = (($Params + "api-version=$ApiVersion") -join '&')
    }

    $restUri += ('_apis/' + $Endpoint + '?' + $paramString)
    if ($PSCmdlet.ShouldProcess($restUri, $Method)) {
        Write-Verbose -Message "Method: $Method"
        $restArgs = @{
            Method  = $Method
            Uri     = $restUri
            Headers = $Headers
        }
        switch ($Method) {
            {
                $_ -eq 'Get' -or
                $_ -eq 'Delete'
            } {
                Write-Verbose -Message 'Executing Get or Delete block'
                if ($OutFile) {
                    $restArgs['OutFile'] = $OutFile
                }
            }
            {
                $_ -eq 'Patch' -or
                $_ -eq 'Put' -or
                $_ -eq 'Post'
            } {
                Write-Verbose -Message 'Executing Patch, Put, or Post block.'
                Write-Verbose -Message "Body:`n$Body"
                if ($restUri -match '.*/workitems/.*') {
                    $restArgs['ContentType'] = 'application/json-patch+json'
                }
                else {
                    $restArgs['ContentType'] = 'application/json'
                }
                $restArgs['Body'] = [System.Text.Encoding]::UTF8.GetBytes($Body)
            }
            Default {
                Write-Error -Message 'An unsupported rest method was attempted.'
            }
        }
        $progress = @{
            Activity = $Method
            Status   = $restUri
        }
        if ($VerbosePreference -ne 'SilentlyContinue') {
            Write-Progress @progress
        }
        if ($OutFile) {
            $progress['CurrentOperation'] = "Downloading $OutFile... "
            if ($VerbosePreference -ne 'SilentlyContinue') {
                Write-Progress @progress
            }
            $ProgressPreference = 'SilentlyContinue'
        }
        if ($NoRetry) {
            $delayCounts = @(0)
        }
        else {
            $delayCounts = @(1, 2, 3, 5, 8, 13, 21)
        }
        foreach ($delay in $delayCounts) {
            try {
                $response = $null
                Write-Verbose -Message "$Method $restUri"
                $restOutput = Invoke-RestMethod @restArgs
                $ProgressPreference = $cachedProgressPreference
                if ($restOutput.value) {
                    $restOutput.value
                }
                elseif ($restOutput.count -eq 0) { }
                elseif ($restOutput -match 'Azure DevOps Services | Sign In') {
                    class AzLoginException : Exception {
                        [System.Object]$Response
                        AzLoginException($Message) : base($Message) {
                            $this.Response = [PSCustomObject]@{
                                StatusCode        = [PSCustomObject]@{
                                    value__ = 401
                                }
                                StatusDescription = $Message
                            }
                        }
                    }
                    throw [AzLoginException]::New('Not authorized.')
                }
                else {
                    $restOutput
                }
                break
            }
            catch {
                $response = $_.Exception.Response
                try {
                    $details = ( $_.ErrorDetails.Message | ConvertFrom-Json -ErrorAction Stop ).message
                }
                catch {
                    $details = $_.ErrorDetails.Message
                }

                if ($response) {
                    $message = "$($response.StatusCode.value__) | $($response.StatusDescription)"
                    if ($details) {
                        $message += " | $details"
                    }
                }
                else {
                    $message = 'Unknown REST error encountered. '
                }
                if (!$NoRetry -and $response.StatusCode.value__ -ne 400) {
                    $message += " | Retrying after $delay seconds..."
                }
                $ProgressPreference = $cachedProgressPreference
                Write-Verbose -Message $message
                $progress['CurrentOperation'] = $message
                if ($VerbosePreference -ne 'SilentlyContinue') {
                    Write-Progress @progress
                }
                if ($OutFile) {
                    $ProgressPreference = 'SilentlyContinue'
                }
                if (!$NoRetry -and $response.StatusCode.value__ -ne 400) {
                    Start-Sleep -Seconds $delay
                }
                else {
                    break
                }
            }
        }
        $ProgressPreference = $cachedProgressPreference
        if ($response) {
            Write-Error -Message "$($response.StatusCode.value__) | $($response.StatusDescription) | $details"
        }
        if ($VerbosePreference -ne 'SilentlyContinue') {
            Write-Progress @progress -Completed
        }
        if ($OutFile) {
            Get-Item -Path $OutFile
        }
    }
}

<#
.SYNOPSIS
Tests various Azure DevOps permissions.
 
.DESCRIPTION
Tests various Azure DevOps permissions.
 
.PARAMETER Project
Projects to test project-scoped permissions with.
 
.PARAMETER CollectionUri
Organization URL.
 
.PARAMETER Pat
Personal Access Token to test.
 
.EXAMPLE
Test-AzDOConnection -Project MyProject -Pat examplePat
 
.NOTES
N/A
#>

function Test-AzDOConnection {
    [CmdletBinding()]
    param (
        [Switch]$NoRetry,
        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [Alias('Name')]
        [String[]]$Project = $env:SYSTEM_TEAMPROJECT,
        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [String]$CollectionUri = $env:SYSTEM_COLLECTIONURI,
        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [string]$Pat = $env:SYSTEM_ACCESSTOKEN
    )

    begin {
        $script:activity = 'Testing Azure DevOps Permissions'
        $script:permissions = @(
            [PSCustomObject]@{
                Scope      = 'Organization'
                Name       = 'Agents'
                Authorized = $false
            }
            [PSCustomObject]@{
                Scope      = 'Organization'
                Name       = 'Organization Info'
                Authorized = $false
            }
            [PSCustomObject]@{
                Scope      = 'Organization'
                Name       = 'Packages'
                Authorized = $false
            }
        )

        $script:restParams = @{
            NoRetry       = $NoRetry
            CollectionUri = $CollectionUri
            Pat           = $Pat
            ErrorAction   = 'SilentlyContinue'
        }

        foreach ($orgPermission in $script:permissions) {
            $status = (
                'Testing ' +
                $CollectionUri.Split('/').Where({ $_ })[-1] +
                '/' +
                $orgPermission.Name +
                ' permissions...'
            )
            Write-Progress -Activity $script:activity -Status $status
            $authorizedPermission = $null
            $authorizedPermission = try {
                switch ($orgPermission.Name) {
                    'Agents' { Get-AzDOAgentPool @script:restParams -Name 'Azure Pipelines' }
                    'Organization Info' { Get-AzDOProject @script:restParams }
                    'Packages' { Get-AzDOPackageFeed @script:restParams }
                }
            }
            catch {
                Write-Verbose -Message $_.Exception.Message
            }
            if ($authorizedPermission) {
                $orgPermission.Authorized = $true
            }
        }
    }

    process {
        foreach ($scope in $Project) {
            $projectPermissions = @(
                [PSCustomObject]@{
                    Scope      = $scope
                    Name       = 'Packages'
                    Authorized = $false
                }
                [PSCustomObject]@{
                    Scope      = $scope
                    Name       = 'Pipelines'
                    Authorized = $false
                }
                [PSCustomObject]@{
                    Scope      = $scope
                    Name       = 'Repositories'
                    Authorized = $false
                }
            )

            $script:restParams['Project'] = $scope

            foreach ($permission in $projectPermissions) {
                $status = "Testing $($permission.Scope)/$($permission.Name) permissions..."
                Write-Progress -Activity $script:activity -Status $status
                $authorizedPermission = $null
                $authorizedPermission = try {
                    switch ($permission.Name) {
                        'Packages' { Get-AzDOPackageFeed @script:restParams }
                        'Pipelines' { Get-AzDOPipeline @script:restParams }
                        'Repositories' { ( Get-AzDORepository @script:restParams ).id }
                    }
                }
                catch {
                    Write-Verbose -Message $_.Exception.Message
                }
                if ($authorizedPermission) {
                    $permission.Authorized = $true
                }
            }

            $script:permissions += $projectPermissions
        }
        Write-Progress -Activity $script:activity -Completed
    }

    end {
        $script:permissions
        $failedPermissions = @( $script:permissions | Where-Object -Property Authorized -NE $true )
        if ($failedPermissions) {
            Write-Error -Message (
                "Not authorized for $($failedPermissions.Count)/$($script:permissions.Count) permissions!"
            )
        }
    }
}

<#
.SYNOPSIS
Adds a comment to a work item.
 
.DESCRIPTION
Adds a comment to a work item.
 
.PARAMETER Id
ID of the work item.
 
.PARAMETER Comment
The comment to add.
 
.PARAMETER Project
Project that the work item is in.
 
.PARAMETER CollectionUri
The full Azure DevOps URL of an organization. Can be automatically populated in a pipeline.
 
.PARAMETER Pat
An Azure DevOps Personal Access Token authorized to edit work items.
 
.EXAMPLE
Get-AzDOWorkItem -Id 12345 -Project MyProject | Add-AzDOWorkItemComment -Comment 'Insert comment here'
 
.LINK
https://learn.microsoft.com/en-us/rest/api/azure/devops/wit/work-items/update
 
.NOTES
N/A
#>

function Add-AzDOWorkItemComment {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, Position = 0, ValueFromPipelineByPropertyName = $true)]
        [String[]]$Id,
        [String]$Comment,
        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [System.Object]$Project = $env:SYSTEM_TEAMPROJECT,
        [String]$CollectionUri = $env:SYSTEM_COLLECTIONURI,
        [String]$Pat = $env:SYSTEM_ACCESSTOKEN
    )

    process {
        Set-AzDOWorkItemField `
            -Id $Id `
            -Name History `
            -Value $Comment `
            -Project $Project `
            -CollectionUri $CollectionUri `
            -Pat $Pat |
            ForEach-Object -Process {
                [PSCustomObject]@{
                    Id      = $_.id
                    Type    = $_.fields | Select-Object -ExpandProperty System.WorkItemType
                    Title   = $_.fields | Select-Object -ExpandProperty System.Title
                    Comment = $_.fields | Select-Object -ExpandProperty System.History
                    Url     = $_.url.Replace(
                        "_apis/wit/workItems/$($_.id)",
                        "_workitems/edit/$($_.id)"
                    )
                }
            }
    }
}

<#
.SYNOPSIS
Adds specific relationships to a work item.
 
.DESCRIPTION
The Add-AzDOWorkItemRelation function adds specific relationships to a work item in Azure DevOps.
 
.PARAMETER Id
The ID of the work item.
 
.PARAMETER RelationType
The types of the relationships to add.
 
.PARAMETER RelatedWorkItemId
The ID of the work item to link to.
 
.PARAMETER NoRetry
Switch to disable retry attempts on API calls.
 
.PARAMETER Project
The name or ID of the project.
 
.PARAMETER CollectionUri
The URI of the Azure DevOps collection.
 
.PARAMETER PAT
The Personal Access Token to authenticate with Azure DevOps.
 
.EXAMPLE
Add-AzDOWorkItemRelation -Id 123 -RelationType 'Parent','Child' -RelatedWorkItemId 456 -CollectionUri 'https://dev.azure.com/mycollection' -Project 'myproject' -PAT 'mypat'
 
.LINK
https://learn.microsoft.com/en-us/rest/api/azure/devops/wit/work-items/update
 
.NOTES
N/A
#>

function Add-AzDOWorkItemRelation {
    [CmdletBinding(SupportsShouldProcess = $true)]
    param (
        [Parameter(Mandatory = $true, Position = 0, ValueFromPipelineByPropertyName = $true)]
        [Int[]]$Id,

        [ValidateSet('Parent', 'Child', 'Successor', 'Predecessor', 'Related')]
        [String[]]$RelationType,

        [Parameter(Mandatory = $true)]
        [Int]$RelatedWorkItemId,

        [Switch]$NoRetry,

        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [System.Object]$Project = $env:SYSTEM_TEAMPROJECT,

        [String]$CollectionUri = $env:SYSTEM_COLLECTIONURI,

        [String]$Pat = $env:SYSTEM_ACCESSTOKEN
    )

    begin {
        $script:AzApiHeaders = @{
            Headers       = Initialize-AzDORestApi -Pat $Pat
            CollectionUri = $CollectionUri
            ApiVersion    = '7.1-preview.3'
        }
    }

    process {
        . $PSScriptRoot/private/Get-AzDOApiProjectName.ps1
        $Project = $Project | Get-AzDOApiProjectName

        $relationTypeMap = & $PSScriptRoot/private/AzDOWorkItemRelationTypeMap.ps1

        foreach ($workItemId in $Id) {
            foreach ($relation in $RelationType) {
                $apiRelationType = $relationTypeMap[$relation]

                if ($PSCmdlet.ShouldProcess($CollectionUri, "Add $relation link from work item $workItemId to work item $RelatedWorkItemId in project $Project")) {
                    $relatedWorkItem = Get-AzDOWorkItem -Id $RelatedWorkItemId -NoRetry:$NoRetry -CollectionUri $CollectionUri -Project $Project -Pat $Pat

                    $body = @(
                        @{
                            op    = 'add'
                            path  = "/relations/-"
                            value = @{
                                rel = $apiRelationType
                                url = $relatedWorkItem.url
                            }
                        }
                    ) | ConvertTo-Json -Compress -AsArray

                    Invoke-AzDORestApiMethod `
                        @script:AzApiHeaders `
                        -Method Patch `
                        -Project $Project `
                        -Endpoint "wit/workitems/$workItemId" `
                        -Body $body `
                        -NoRetry:$NoRetry
                }
            }
        }
    }
}

<#
.SYNOPSIS
Formats a work item object for display.
 
.DESCRIPTION
This function takes a work item object and returns a custom object with the following properties:
- Id
- Type
- Title
- State
- Project
- AreaPath
- IterationPath
- AssignedTo
- Url
 
.PARAMETER WorkItem
The work item object to format.
 
.EXAMPLE
Get-AzDOWorkItem -Id 12345 | Format-AzDOWorkItem
 
.NOTES
N/A
#>

function Format-AzDOWorkItem {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)]
        [ValidateScript({
                $_.PSObject.Properties.Name -contains 'id' -and
                $_.PSObject.Properties.Name -contains 'fields' -and
                $_.PSObject.Properties.Name -contains '_links'
            })]
        [System.Object]$WorkItem,
        [Switch]$Expand
    )

    process {
        try {
            $basicObject = @{
                Id    = $WorkItem.id
                Type  = $WorkItem.fields.'System.WorkItemType'
                Title = $WorkItem.fields.'System.Title'
            }
            $url = @{
                Url = $WorkItem._links.html.href
            }
            $finalObject = $basicObject
            if ($Expand) {
                $expandedProperties = @{
                    State         = $WorkItem.fields.'System.State'
                    Project       = $WorkItem.fields.'System.TeamProject'
                    AreaPath      = $WorkItem.fields.'System.AreaPath'
                    IterationPath = $WorkItem.fields.'System.IterationPath'
                    AssignedTo    = $WorkItem.fields.'System.AssignedTo'
                }
                foreach ($key in $expandedProperties.Keys) {
                    $finalObject[$key] = $expandedProperties[$key]
                }
            }
            foreach ($key in $url.Keys) {
                $finalObject[$key] = $url[$key]
            }
            [PSCustomObject]$finalObject
        }
        catch {
            Write-Error -Message "Failed to format work item: $($_.Exception.Message)"
        }
    }
}

<#
.SYNOPSIS
Get a Work Item's info.
 
.DESCRIPTION
Get a Work Item's info.
 
.PARAMETER Id
ID of the work item.
 
.PARAMETER Title
Title of the work item.
 
.PARAMETER Project
Project that the work item is in.
 
.PARAMETER CollectionUri
The full Azure DevOps URL of an organization. Can be automatically populated in a pipeline.
 
.PARAMETER Pat
An Azure DevOps Personal Access Token authorized to read work items.
 
.EXAMPLE
Get-AzDOWorkItem -Id 12345 -Project MyProject
 
.LINK
https://learn.microsoft.com/en-us/rest/api/azure/devops/wit/work-items/get-work-item
 
.LINK
https://learn.microsoft.com/en-us/rest/api/azure/devops/wit/wiql/query-by-wiql
 
.NOTES
N/A
#>

function Get-AzDOWorkItem {
    [CmdletBinding(DefaultParameterSetName = 'ID')]
    param (
        [Parameter(ParameterSetName = 'ID', Mandatory = $true, Position = 0, ValueFromPipelineByPropertyName = $true)]
        [Int[]]$Id,
        [Parameter(ParameterSetName = 'Title', Mandatory = $true, Position = 0)]
        [String]$Title,
        [Switch]$NoRetry,
        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [String]$Project = $env:SYSTEM_TEAMPROJECT,
        [String]$CollectionUri = $env:SYSTEM_COLLECTIONURI,
        [String]$Pat = $env:SYSTEM_ACCESSTOKEN
    )

    begin {
        $script:AzApiHeaders = @{
            Headers       = Initialize-AzDORestApi -Pat $Pat
            CollectionUri = $CollectionUri
            ApiVersion    = '7.1-preview.2'
        }
    }

    process {
        if ($Title) {
            $body = @{
                query = "SELECT [System.Id] FROM workitems WHERE [System.Title] CONTAINS '$Title'"
            } | ConvertTo-Json -Compress

            $Id = @(
                Invoke-AzDORestApiMethod `
                    @script:AzApiHeaders `
                    -Method Post `
                    -Project $Project `
                    -Endpoint "wit/wiql" `
                    -Body $body `
                    -NoRetry:$NoRetry |
                    Select-Object -ExpandProperty workItems |
                    Select-Object -ExpandProperty id
            )
            if (!$Id) {
                Write-Warning -Message "No work items found with title $Title in project $Project."
            }
        }

        foreach ($item in $Id) {
            $workItem = Invoke-AzDORestApiMethod `
                @script:AzApiHeaders `
                -Method Get `
                -Project $Project `
                -Endpoint "wit/workitems/$item" `
                -Params @(
                    '$expand=All'
                ) `
                -NoRetry:$NoRetry
            $workItem | Add-Member `
                -MemberType NoteProperty `
                -Name project `
                -Value $workItem.fields.'System.TeamProject'
            $workItem
        }
    }
}

<#
.SYNOPSIS
Gets work item types for a project.
 
.DESCRIPTION
Gets work item types for a project.
 
.PARAMETER Type
Filter by work item type.
 
.PARAMETER NoRetry
Don't retry failed calls.
 
.PARAMETER Project
Project to list work item types for.
 
.PARAMETER CollectionUri
The full Azure DevOps URL of an organization. Can be automatically populated in a pipeline.
 
.PARAMETER Pat
An Azure DevOps Personal Access Token authorized to read work items.
 
.EXAMPLE
Get-AzDOWorkItemType -Project 'MyProject'
 
.LINK
https://learn.microsoft.com/en-us/rest/api/azure/devops/wit/work-item-types/list
 
.NOTES
N/A
#>

function Get-AzDOWorkItemType {
    [CmdletBinding()]
    [OutputType([System.Object[]])]
    param (
        [String[]]$Type,
        [Switch]$NoRetry,
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [Alias('Id')]
        [String]$Project = $env:SYSTEM_TEAMPROJECT,
        [String]$CollectionUri = $env:SYSTEM_COLLECTIONURI,
        [String]$Pat = $env:SYSTEM_ACCESSTOKEN
    )

    begin {
        $script:AzApiHeaders = @{
            Headers       = Initialize-AzDORestApi -Pat $Pat
            CollectionUri = $CollectionUri
            ApiVersion    = '7.1-preview.2'
        }
    }

    process {
        $processId = Get-AzDOProject -Name $Project -NoRetry:$NoRetry -CollectionUri $CollectionUri -Pat $Pat |
            Select-Object -ExpandProperty capabilities |
            Select-Object -ExpandProperty processTemplate |
            Select-Object -ExpandProperty templateTypeId

        $types = @(
            Invoke-AzDORestApiMethod `
                @script:AzApiHeaders `
                -Method Get `
                -Endpoint "work/processes/$processId/workitemtypes" `
                -NoRetry:$NoRetry
        )
        if ($Type) {
            $types | Where-Object -FilterScript { $Type -contains $_.name }
        }
        else {
            $types
        }
    }
}

<#
.SYNOPSIS
Get a Work Item's info.
 
.DESCRIPTION
Get a Work Item's info.
 
.PARAMETER Id
ID of the work item.
 
.PARAMETER Project
Project that the work item is in.
 
.PARAMETER CollectionUri
The full Azure DevOps URL of an organization. Can be automatically populated in a pipeline.
 
.PARAMETER Pat
An Azure DevOps Personal Access Token authorized to read work items.
 
.EXAMPLE
Get-AzDOWorkItem -Id 12345 -Project MyProject
 
.LINK
https://learn.microsoft.com/en-us/rest/api/azure/devops/wit/work-items/create
 
.NOTES
N/A
#>

function New-AzDOWorkItem {
    [CmdletBinding(SupportsShouldProcess = $true)]
    param (
        [Parameter(Mandatory = $true, Position = 0)]
        [String[]]$Title,
        [String]$Type = 'User Story',
        [String]$AreaPath,
        [String]$IterationPath,
        [String]$Description = 'Created via AzDOCmd\New-AzDOWorkItem',
        [Int]$ParentId,
        [Int]$ChildId,
        [Switch]$SuppressNotifications,
        [Switch]$NoRetry,
        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [Alias('Id')]
        [String]$Project = $env:SYSTEM_TEAMPROJECT,
        [String]$CollectionUri = $env:SYSTEM_COLLECTIONURI,
        [String]$Pat = $env:SYSTEM_ACCESSTOKEN
    )

    begin {
        $script:AzApiHeaders = @{
            Headers       = Initialize-AzDORestApi -Pat $Pat
            CollectionUri = $CollectionUri
            ApiVersion    = '7.1-preview.3'
        }
    }

    process {
        $validType = Get-AzDOWorkItemType `
            -Type $Type `
            -NoRetry:$NoRetry `
            -Project $Project `
            -CollectionUri $CollectionUri `
            -Pat $Pat
        if (!$validType) {
            throw "Invalid work item type: $Type"
        }

        foreach ($item in $Title) {
            if (
                $PSCmdlet.ShouldProcess(
                    $CollectionUri, "Create work item $item of type $Type in project $Project"
                )
            ) {
                $descriptionFieldName = if ($Type -eq 'Bug') {
                    'Microsoft.VSTS.TCM.ReproSteps'
                }
                else {
                    'System.Description'
                }
                $body = @(
                    [PSCustomObject]@{
                        op    = 'add'
                        path  = '/fields/System.Title'
                        from  = $null
                        value = $item
                    },
                    [PSCustomObject]@{
                        op    = 'add'
                        path  = "/fields/$descriptionFieldName"
                        value = $Description
                    }
                )
                if ($AreaPath) {
                    if ($AreaPath -notlike "$Project*") {
                        $AreaPath = $Project + '\' + $AreaPath
                    }
                    $body += [PSCustomObject]@{
                        op    = 'add'
                        path  = '/fields/System.AreaPath'
                        value = $AreaPath
                    }
                }
                if ($IterationPath) {
                    if ($IterationPath -notlike "$Project*") {
                        $IterationPath = $Project + '\' + $IterationPath
                    }
                    $body += [PSCustomObject]@{
                        op    = 'add'
                        path  = '/fields/System.IterationPath'
                        value = $IterationPath
                    }
                }
                if ($ParentId) {
                    $body += [PSCustomObject]@{
                        op    = 'add'
                        path  = '/relations/-'
                        value = @{
                            rel = 'System.LinkTypes.Hierarchy-Reverse'
                            url = "$CollectionUri/$Project/_apis/wit/workItems/$ParentId"
                        }
                    }
                }
                if ($ChildId) {
                    $body += [PSCustomObject]@{
                        op    = 'add'
                        path  = '/relations/-'
                        value = @{
                            rel = 'System.LinkTypes.Hierarchy-Forward'
                            url = "$CollectionUri/$Project/_apis/wit/workItems/$ChildId"
                        }
                    }
                }

                $workItem = Invoke-AzDORestApiMethod `
                    @script:AzApiHeaders `
                    -Method Post `
                    -Project $Project `
                    -Endpoint "wit/workitems/`$$([Uri]::EscapeDataString($Type))" `
                    -Body $( $body | ConvertTo-Json -AsArray -Compress ) `
                    -Params @(
                        "suppressNotifications=$SuppressNotifications"
                        '$expand=All'
                    ) `
                    -NoRetry:$NoRetry
                $workItem | Add-Member `
                    -MemberType NoteProperty `
                    -Name project `
                    -Value $workItem.fields.'System.TeamProject'
                $workItem
            }
        }
    }
}

<#
.SYNOPSIS
Removes a specific relationship from a work item.
 
.DESCRIPTION
The Remove-AzDOWorkItemRelation function removes a specific relationship from a work item in Azure DevOps.
 
.PARAMETER Id
The ID of the work item.
 
.PARAMETER RelationType
The type of the relationship to remove.
 
.PARAMETER NoRetry
Switch to disable retry attempts on API calls.
 
.PARAMETER Project
The name or ID of the project.
 
.PARAMETER CollectionUri
The URI of the Azure DevOps collection.
 
.PARAMETER PAT
The Personal Access Token to authenticate with Azure DevOps.
 
.EXAMPLE
Remove-AzDOWorkItemRelation -Id 12345 -RelationType Parent -Project MyProject
 
.LINK
https://learn.microsoft.com/en-us/rest/api/azure/devops/wit/work-items/update
 
.NOTES
N/A
#>

function Remove-AzDOWorkItemRelation {
    [CmdletBinding(SupportsShouldProcess = $true)]
    param (
        [Parameter(Mandatory = $true, Position = 0, ValueFromPipelineByPropertyName = $true)]
        [Int[]]$Id,

        [ValidateSet('Parent', 'Child', 'Successor', 'Predecessor', 'Related')]
        [String]$RelationType = ('Parent', 'Child', 'Successor', 'Predecessor', 'Related'),

        [Switch]$NoRetry,

        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [System.Object]$Project = $env:SYSTEM_TEAMPROJECT,

        [String]$CollectionUri = $env:SYSTEM_COLLECTIONURI,

        [String]$Pat = $env:SYSTEM_ACCESSTOKEN
    )

    begin {
        $script:AzApiHeaders = @{
            Headers       = Initialize-AzDORestApi -Pat $Pat
            CollectionUri = $CollectionUri
            ApiVersion    = '7.1-preview.3'
        }
    }

    process {
        . $PSScriptRoot/private/Get-AzDOApiProjectName.ps1
        $Project = $Project | Get-AzDOApiProjectName

        $relationTypeMap = & $PSScriptRoot/private/AzDOWorkItemRelationTypeMap.ps1

        $apiRelationType = $relationTypeMap[$RelationType]

        foreach ($workItemId in $Id) {
            $workItem = Get-AzDOWorkItem `
                -Id $workItemId `
                -Project $Project `
                -NoRetry:$NoRetry `
                -CollectionUri $CollectionUri `
                -Pat $Pat

            # Find the index of the link to remove
            $linkIndex = @(
                $workItem.relations |
                    Where-Object { $_.rel -eq $apiRelationType } |
                    ForEach-Object { $workItem.relations.IndexOf($_) }
            )

            if ($null -ne $linkIndex) {
                $body = @()
                $body += foreach ($index in $linkIndex) {
                    @{
                        op   = 'remove'
                        path = "/relations/$index"
                    }
                }

                if ($PSCmdlet.ShouldProcess($CollectionUri, "Remove $RelationType link(s) from work item $workItemId in project $Project")) {
                    Invoke-AzDORestApiMethod `
                        @script:AzApiHeaders `
                        -Method Patch `
                        -Project $Project `
                        -Endpoint "wit/workitems/$WorkItemId" `
                        -Body ( $body | ConvertTo-Json -Compress -AsArray ) `
                        -NoRetry:$NoRetry
                }
            }
            else {
                Write-Warning -Message "No $RelationType link found on work item $workItemId in project $Project."
            }
        }
    }
}

<#
.SYNOPSIS
Updates a field in a work item.
 
.DESCRIPTION
Updates a field in a work item.
 
.PARAMETER Id
ID of the work item.
 
.PARAMETER Field
The work item field to update.
 
.PARAMETER Value
The value to populate the field with.
 
.PARAMETER Project
Project that the work item is in.
 
.PARAMETER CollectionUri
The full Azure DevOps URL of an organization. Can be automatically populated in a pipeline.
 
.PARAMETER Pat
An Azure DevOps Personal Access Token authorized to edit work items.
 
.EXAMPLE
Get-AzDOWorkItem -Id 12345 -Project MyProject | Set-AzDOWorkItemField -Name Title -Value 'A better title'
 
.LINK
https://learn.microsoft.com/en-us/rest/api/azure/devops/wit/work-items/update
 
.NOTES
General notes
#>

function Set-AzDOWorkItemField {
    [CmdletBinding(SupportsShouldProcess = $true)]
    param (
        [Parameter(Mandatory = $true, Position = 0, ValueFromPipelineByPropertyName = $true)]
        [Int[]]$Id,
        [Alias('Field')]
        [String]$Name,
        [String]$Value,
        [Switch]$NoRetry,
        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [System.Object]$Project = $env:SYSTEM_TEAMPROJECT,
        [String]$CollectionUri = $env:SYSTEM_COLLECTIONURI,
        [String]$Pat = $env:SYSTEM_ACCESSTOKEN
    )

    begin {
        $script:AzApiHeaders = @{
            Headers       = Initialize-AzDORestApi -Pat $Pat
            CollectionUri = $CollectionUri
            ApiVersion    = '7.1-preview.3'
        }
    }

    process {
        . $PSScriptRoot/private/Get-AzDOApiProjectName.ps1
        $Project = $Project | Get-AzDOApiProjectName

        foreach ($number in $id) {
            if ($PSCmdlet.ShouldProcess($CollectionUri, "Update work item $number with field $Name to value $Value in project $Project")) {
                $body = @(
                    @{
                        op    = 'add'
                        path  = "/fields/System.$Name"
                        value = $Value
                    }
                ) | ConvertTo-Json
                $body = "[`n$body`n]"

                Invoke-AzDORestApiMethod `
                    @script:AzApiHeaders `
                    -Method Patch `
                    -Project $Project `
                    -Endpoint "wit/workitems/$number" `
                    -Body $body `
                    -NoRetry:$NoRetry
            }
        }
    }
}