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 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 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 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-BuildPipeline. .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', [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' } $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-AzApiProjectName.ps1 $Project = $Project | Get-AzApiProjectName 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-BuildPipeline. .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-BuildPipeline -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 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 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()] 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 } } 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 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()] 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 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()] 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 Invoke-AzDORestApiMethod ` @script:AzApiHeaders ` -Method Put ` -Project $Project ` -Endpoint "release/definitions/$PipelineId" ` -Body ( Get-Content -Path $JsonFilePath -Encoding UTF8 | Out-String ) ` -NoRetry:$NoRetry } } <# .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-Host -Object ( $currentAzDOConnection | Format-List | Out-String ) $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 } '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 -Name $scope ).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 Invoke-AzDORestApiMethod ` @script:AzApiHeaders ` -Method Patch ` -Project $Project ` -Endpoint "wit/workitems/$workItemId" ` -Body $body ` -NoRetry:$NoRetry } } } } } <# .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, ValueFromPipeline = $true)] [Int[]]$Id, [Parameter(ParameterSetName = 'Title', Mandatory = $true, Position = 0)] [String]$Title, [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.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" ` -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 -CollectionUri $CollectionUri -Project $Project -WorkItemId $workItemId -PAT $PAT # Find the index of the link to remove $linkIndex = @( $workItem.relations | Where-Object { $_.rel -eq $apiRelationType } | ForEach-Object { $workItem.relations.IndexOf($_) } ) if ($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 ) ` -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 } } } } |