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 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 ) Invoke-AzDORestApiMethod ` -Method Get ` -CollectionUri $CollectionUri ` -Project $Project ` -Endpoint "git/repositories/$Name" ` -ApiVersion '7.1-preview.1' ` -Headers ( Initialize-AzDORestApi -Pat $Pat ) ` -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 += "$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" $output = Invoke-RestMethod @restArgs $ProgressPreference = $cachedProgressPreference if ($output.value) { $output.value } elseif ($output.count -eq 0) { } elseif ($output -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 { $output } 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!" ) } } } |