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 } } } } |