From d9b6eb830a04dac9e5581c11881705926a79fbe1 Mon Sep 17 00:00:00 2001 From: Nico Orschel Date: Fri, 24 Oct 2025 10:43:09 +0200 Subject: [PATCH 1/6] Add API version compatibility warning and fix find orphan picklists in Get-ADOPicklist for AzD Server --- Functions/AzureDevOps/Get-ADOField.ps1 | 4 + Functions/AzureDevOps/Get-ADOPicklist.ps1 | 105 ++++++++++++---------- 2 files changed, 62 insertions(+), 47 deletions(-) diff --git a/Functions/AzureDevOps/Get-ADOField.ps1 b/Functions/AzureDevOps/Get-ADOField.ps1 index 5393f512..fd4c5beb 100644 --- a/Functions/AzureDevOps/Get-ADOField.ps1 +++ b/Functions/AzureDevOps/Get-ADOField.ps1 @@ -76,6 +76,10 @@ } process { + if ($ApiVersion -like '5.*' -and $sr) { + Write-Warning "The API version '$ApiVersion' may not be compatible with field operations. Consider using '7.0' or later." + } + # First, construct a base URI. It's made up of: $uriBase = "$Server".TrimEnd('/'), # * The server $Organization, # * The organization diff --git a/Functions/AzureDevOps/Get-ADOPicklist.ps1 b/Functions/AzureDevOps/Get-ADOPicklist.ps1 index adf221cf..9638f172 100644 --- a/Functions/AzureDevOps/Get-ADOPicklist.ps1 +++ b/Functions/AzureDevOps/Get-ADOPicklist.ps1 @@ -1,5 +1,4 @@ -function Get-ADOPicklist -{ +function Get-ADOPicklist { <# .Synopsis Gets picklists from Azure DevOps. @@ -16,43 +15,43 @@ .Link https://docs.microsoft.com/en-us/rest/api/azure/devops/processes/lists/get #> - [OutputType('PSDevOps.Project','PSDevOps.Property')] - [CmdletBinding(DefaultParameterSetName='work/processes/lists')] + [OutputType('PSDevOps.Project', 'PSDevOps.Property')] + [CmdletBinding(DefaultParameterSetName = 'work/processes/lists')] param( - # The Organization - [Parameter(Mandatory,ParameterSetName='work/processes/lists',ValueFromPipelineByPropertyName)] - [Parameter(Mandatory,ParameterSetName='work/processes/lists/{PickListID}',ValueFromPipelineByPropertyName)] - [Parameter(Mandatory,ParameterSetName='Orphan')] - [Alias('Org')] - [string] - $Organization, - - # The Picklist Identifier. - [Parameter(Mandatory,ParameterSetName='work/processes/lists/{PickListID}',ValueFromPipelineByPropertyName)] - [string] - $PickListID, - - # The name of the picklist - [Parameter(ValueFromPipelineByPropertyName)] - [string] - $PicklistName = '*', - - # If set, will return orphan picklists. These picklists are not associated with any field. - [Parameter(Mandatory,ParameterSetName='Orphan')] - [switch] - $Orphan, - - # The server. By default https://dev.azure.com/. - # To use against TFS, provide the tfs server URL (e.g. http://tfsserver:8080/tfs). - [Parameter(ValueFromPipelineByPropertyName)] - [uri] - $Server = "https://dev.azure.com/", - - # The api version. By default, 5.1-preview. - # If targeting TFS, this will need to change to match your server version. - # See: https://docs.microsoft.com/en-us/azure/devops/integrate/concepts/rest-api-versioning?view=azure-devops - [string] - $ApiVersion = "5.1-preview" + # The Organization + [Parameter(Mandatory, ParameterSetName = 'work/processes/lists', ValueFromPipelineByPropertyName)] + [Parameter(Mandatory, ParameterSetName = 'work/processes/lists/{PickListID}', ValueFromPipelineByPropertyName)] + [Parameter(Mandatory, ParameterSetName = 'Orphan')] + [Alias('Org')] + [string] + $Organization, + + # The Picklist Identifier. + [Parameter(Mandatory, ParameterSetName = 'work/processes/lists/{PickListID}', ValueFromPipelineByPropertyName)] + [string] + $PickListID, + + # The name of the picklist + [Parameter(ValueFromPipelineByPropertyName)] + [string] + $PicklistName = '*', + + # If set, will return orphan picklists. These picklists are not associated with any field. + [Parameter(Mandatory, ParameterSetName = 'Orphan')] + [switch] + $Orphan, + + # The server. By default https://dev.azure.com/. + # To use against TFS, provide the tfs server URL (e.g. http://tfsserver:8080/tfs). + [Parameter(ValueFromPipelineByPropertyName)] + [uri] + $Server = "https://dev.azure.com/", + + # The api version. By default, 5.1-preview. + # If targeting TFS, this will need to change to match your server version. + # See: https://docs.microsoft.com/en-us/azure/devops/integrate/concepts/rest-api-versioning?view=azure-devops + [string] + $ApiVersion = "5.1-preview" ) dynamicParam { . $GetInvokeParameters -DynamicParameter } @@ -65,18 +64,30 @@ } process { if ($Orphan) { - $allPicklists = Get-ADOPicklist -Organization $organization - $allUsedPicklists = Get-ADOField @invokeParams -Organization $Organization | + $allPicklists = @() + $allUsedPicklists = @() + if ($Server -eq 'https://dev.azure.com') { + $allPicklists = Get-ADOPicklist -Organization $organization + $allUsedPicklists = Get-ADOField @invokeParams -Organization $Organization | Where-Object { $_.IsPicklist } | Select-Object -ExpandProperty PicklistID + } + else { + Write-Verbose "AzD Server currently is: $Server/$organization" + $allPicklists = Get-ADOPicklist -Organization $organization -Server $Server -Apiversion $apiVersion + $allUsedPicklists = Get-ADOField @invokeParams -Organization $Organization -Server $Server -Apiversion $apiVersion | + Where-Object { $_.IsPicklist } | + Select-Object -ExpandProperty PicklistID + } + $allPicklists | - Where-Object PicklistID -NotIn $allUsedPicklists + Where-Object PicklistID -NotIn $allUsedPicklists return } $in = $_ $psParameterSet = $psCmdlet.ParameterSetName - $q.Enqueue(@{PSParameterSet=$psParameterSet} + $PSBoundParameters) + $q.Enqueue(@{PSParameterSet = $psParameterSet } + $PSBoundParameters) } end { $c, $t, $progId = 0, $q.Count, [Random]::new().Next() @@ -84,7 +95,7 @@ . $dq $q $uri = - "$(@( + "$(@( "$server".TrimEnd('/') # * The Server $Organization @@ -106,7 +117,7 @@ $typeName = @($psParameterSet -split '/' -notlike '{*}')[-1] -replace - 's$', '' -replace 'list', 'Picklist' + 's$', '' -replace 'list', 'Picklist' if ($psParameterSet -like '*/{PicklistId}') { $typeName = "PickList.Detail" @@ -114,11 +125,11 @@ $additionalProperty = @{ Organization = $Organization - Server = $Server + Server = $Server } Invoke-ADORestAPI @invokeParams -uri $uri -PSTypeName "$Organization.$typeName", - "PSDevOps.$typeName" -Property $additionalProperty | - Where-Object { $_.Name -like $PicklistName } + "PSDevOps.$typeName" -Property $additionalProperty | + Where-Object { $_.Name -like $PicklistName } } Write-Progress "Getting" "[$t/$t]" -Completed -Id $progId From f3894b327b29b5a1308c2aafcb1aa3cd9849e31e Mon Sep 17 00:00:00 2001 From: Nico Orschel Date: Fri, 24 Oct 2025 10:45:46 +0200 Subject: [PATCH 2/6] Add warning for potential API version compatibility issues in Get-ADOPicklist --- Functions/AzureDevOps/Get-ADOPicklist.ps1 | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Functions/AzureDevOps/Get-ADOPicklist.ps1 b/Functions/AzureDevOps/Get-ADOPicklist.ps1 index 9638f172..aad1583c 100644 --- a/Functions/AzureDevOps/Get-ADOPicklist.ps1 +++ b/Functions/AzureDevOps/Get-ADOPicklist.ps1 @@ -63,6 +63,10 @@ $q = [Collections.Queue]::new() } process { + if ($ApiVersion -like '5.*' -and $sr) { + Write-Warning "The API version '$ApiVersion' may not be compatible with field operations. Consider using '7.0' or later." + } + if ($Orphan) { $allPicklists = @() $allUsedPicklists = @() From 0573cace1b936c10722943eb009ad57156e043b1 Mon Sep 17 00:00:00 2001 From: Nico Orschel Date: Fri, 24 Oct 2025 10:47:24 +0200 Subject: [PATCH 3/6] Refactor formatting and improve parameter definitions in Remove-ADOPermission function --- .../AzureDevOps/Remove-ADOPermission.ps1 | 452 +++++++++--------- 1 file changed, 230 insertions(+), 222 deletions(-) diff --git a/Functions/AzureDevOps/Remove-ADOPermission.ps1 b/Functions/AzureDevOps/Remove-ADOPermission.ps1 index 67b20b99..a47742dc 100644 --- a/Functions/AzureDevOps/Remove-ADOPermission.ps1 +++ b/Functions/AzureDevOps/Remove-ADOPermission.ps1 @@ -1,5 +1,4 @@ -function Remove-ADOPermission -{ +function Remove-ADOPermission { <# .Synopsis Removes Azure DevOps Permissions @@ -12,153 +11,153 @@ .Link https://docs.microsoft.com/en-us/azure/devops/organizations/security/namespace-reference #> - [CmdletBinding(SupportsShouldProcess,ConfirmImpact='High')] - [Diagnostics.CodeAnalysis.SuppressMessageAttribute("Test-ForParameterSetAmbiguity", "", Justification="Ambiguity Desired.")] + [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("Test-ForParameterSetAmbiguity", "", Justification = "Ambiguity Desired.")] [OutputType('PSDevOps.SecurityNamespace', 'PSDevOps.AccessControlList')] param( - # The Organization. - [Parameter(Mandatory,ValueFromPipelineByPropertyName)] - [Alias('Org')] - [string] - $Organization, - - # The Project ID. - # If this is provided without anything else, will get permissions for the projectID - [Parameter(Mandatory,ValueFromPipelineByPropertyName,ParameterSetName='Project')] - [Parameter(ValueFromPipelineByPropertyName,ParameterSetName='Analytics')] - [Parameter(ValueFromPipelineByPropertyName,ParameterSetName='EndpointID')] - [Parameter(Mandatory,ValueFromPipelineByPropertyName,ParameterSetName='AreaPath')] - [Parameter(Mandatory,ValueFromPipelineByPropertyName,ParameterSetName='Dashboard')] - [Parameter(Mandatory,ValueFromPipelineByPropertyName,ParameterSetName='IterationPath')] - [Parameter(Mandatory,ValueFromPipelineByPropertyName,ParameterSetName='Tagging')] - [Parameter(Mandatory,ValueFromPipelineByPropertyName,ParameterSetName='ManageTFVC')] - [Parameter(Mandatory,ValueFromPipelineByPropertyName,ParameterSetName='BuildDefinition')] - [Parameter(Mandatory,ValueFromPipelineByPropertyName,ParameterSetName='BuildPermission')] - [Parameter(Mandatory,ValueFromPipelineByPropertyName,ParameterSetName='RepositoryID')] - [Parameter(Mandatory,ValueFromPipelineByPropertyName,ParameterSetName='ProjectRepository')] - [Alias('Project')] - [string] - $ProjectID, - - # If provided, will set permissions related to a given teamID. ( see Get-ADOTeam) - [Parameter(ValueFromPipelineByPropertyName,ParameterSetName='Dashboard')] - [string] - $TeamID, - - # If provided, will set permissions related to an Area Path. ( see Get-ADOAreaPath ) - [Parameter(Mandatory,ValueFromPipelineByPropertyName,ParameterSetName='AreaPath')] - [string] - $AreaPath, - - # If provided, will set permissions related to an Iteration Path. ( see Get-ADOIterationPath ) - [Parameter(Mandatory,ValueFromPipelineByPropertyName,ParameterSetName='IterationPath')] - [string] - $IterationPath, - - # The Build Definition ID - [Parameter(Mandatory,ValueFromPipelineByPropertyName,ParameterSetName='BuildDefinition')] - [string] - $DefinitionID, - - # The path to the build. - [Parameter(ValueFromPipelineByPropertyName,ParameterSetName='BuildDefinition')] - [string] - $BuildPath ='/', - - # If set, will set build and release permissions for a given project. - [Parameter(Mandatory,ValueFromPipelineByPropertyName,ParameterSetName='BuildPermission')] - [switch] - $BuildPermission, - - # If set, will set permissions for repositories within a project - [Parameter(Mandatory,ValueFromPipelineByPropertyName,ParameterSetName='ProjectRepository')] - [Alias('ProjectRepositories')] - [switch] - $ProjectRepository, - - # If provided, will set permissions for a given repositoryID - [Parameter(Mandatory,ValueFromPipelineByPropertyName,ParameterSetName='RepositoryID')] - [string] - $RepositoryID, - - # If provided, will set permissions for a given branch within a repository - [Parameter(ValueFromPipelineByPropertyName,ParameterSetName='RepositoryID')] - [Parameter(ValueFromPipelineByPropertyName,ParameterSetName='AllRepositories')] - [string] - $BranchName, - - # If set, will set permissions for all repositories within a project - [Parameter(Mandatory,ValueFromPipelineByPropertyName,ParameterSetName='AllRepositories')] - [Alias('AllRepositories')] - [switch] - $AllRepository, - - # If set, will set permissions for tagging related to the current project. - [Parameter(Mandatory,ValueFromPipelineByPropertyName,ParameterSetName='Tagging')] - [switch] - $Tagging, - - - # If set, will set permissions for Team Foundation Version Control related to the current project. - [Parameter(Mandatory,ValueFromPipelineByPropertyName,ParameterSetName='ManageTFVC')] - [switch] - $ManageTFVC, - - # If set, will set permissions for Delivery Plans. - [Parameter(Mandatory,ValueFromPipelineByPropertyName,ParameterSetName='Plan')] - [switch] - $Plan, - - # If set, will set dashboard permissions related to the current project. - [Parameter(Mandatory,ValueFromPipelineByPropertyName,ParameterSetName='Dashboard')] - [Alias('Dashboards')] - [switch] - $Dashboard, - - # If set, will set endpoint permissions related to a particular endpoint. - [Parameter(Mandatory,ValueFromPipelineByPropertyName,ParameterSetName='EndpointID')] - [string] - $EndpointID, + # The Organization. + [Parameter(Mandatory, ValueFromPipelineByPropertyName)] + [Alias('Org')] + [string] + $Organization, + + # The Project ID. + # If this is provided without anything else, will get permissions for the projectID + [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'Project')] + [Parameter(ValueFromPipelineByPropertyName, ParameterSetName = 'Analytics')] + [Parameter(ValueFromPipelineByPropertyName, ParameterSetName = 'EndpointID')] + [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'AreaPath')] + [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'Dashboard')] + [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'IterationPath')] + [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'Tagging')] + [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'ManageTFVC')] + [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'BuildDefinition')] + [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'BuildPermission')] + [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'RepositoryID')] + [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'ProjectRepository')] + [Alias('Project')] + [string] + $ProjectID, + + # If provided, will set permissions related to a given teamID. ( see Get-ADOTeam) + [Parameter(ValueFromPipelineByPropertyName, ParameterSetName = 'Dashboard')] + [string] + $TeamID, + + # If provided, will set permissions related to an Area Path. ( see Get-ADOAreaPath ) + [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'AreaPath')] + [string] + $AreaPath, + + # If provided, will set permissions related to an Iteration Path. ( see Get-ADOIterationPath ) + [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'IterationPath')] + [string] + $IterationPath, + + # The Build Definition ID + [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'BuildDefinition')] + [string] + $DefinitionID, + + # The path to the build. + [Parameter(ValueFromPipelineByPropertyName, ParameterSetName = 'BuildDefinition')] + [string] + $BuildPath = '/', + + # If set, will set build and release permissions for a given project. + [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'BuildPermission')] + [switch] + $BuildPermission, + + # If set, will set permissions for repositories within a project + [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'ProjectRepository')] + [Alias('ProjectRepositories')] + [switch] + $ProjectRepository, + + # If provided, will set permissions for a given repositoryID + [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'RepositoryID')] + [string] + $RepositoryID, + + # If provided, will set permissions for a given branch within a repository + [Parameter(ValueFromPipelineByPropertyName, ParameterSetName = 'RepositoryID')] + [Parameter(ValueFromPipelineByPropertyName, ParameterSetName = 'AllRepositories')] + [string] + $BranchName, + + # If set, will set permissions for all repositories within a project + [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'AllRepositories')] + [Alias('AllRepositories')] + [switch] + $AllRepository, + + # If set, will set permissions for tagging related to the current project. + [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'Tagging')] + [switch] + $Tagging, + + + # If set, will set permissions for Team Foundation Version Control related to the current project. + [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'ManageTFVC')] + [switch] + $ManageTFVC, + + # If set, will set permissions for Delivery Plans. + [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'Plan')] + [switch] + $Plan, + + # If set, will set dashboard permissions related to the current project. + [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'Dashboard')] + [Alias('Dashboards')] + [switch] + $Dashboard, + + # If set, will set endpoint permissions related to a particular endpoint. + [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'EndpointID')] + [string] + $EndpointID, - # The Security Namespace ID. - [Parameter(Mandatory,ValueFromPipelineByPropertyName, - ParameterSetName='accesscontrolentries/{NamespaceId}')] - [string] - $NamespaceID, - - # The Security Token. - [Parameter(Mandatory,ValueFromPipelineByPropertyName, - ParameterSetName='accesscontrolentries/{NamespaceId}')] - [string] - $SecurityToken, - - # One or more descriptors - [Parameter(ValueFromPipelineByPropertyName, - ParameterSetName='accesscontrolentries/{NamespaceId}')] - [string[]] - $Descriptor, - - # One or more identities. Identities will be converted into descriptors. - [Parameter(ValueFromPipelineByPropertyName)] - [string[]] - $Identity, - - # One or more allow permissions. - [Parameter(ValueFromPipelineByPropertyName)] - [string[]] - $Permission, - - # The server. By default https://dev.azure.com/. - # To use against TFS, provide the tfs server URL (e.g. http://tfsserver:8080/tfs). - [Parameter(ValueFromPipelineByPropertyName)] - [uri] - $Server = "https://dev.azure.com/", - - # The api version. By default, 5.1-preview. - # If targeting TFS, this will need to change to match your server version. - # See: https://docs.microsoft.com/en-us/azure/devops/integrate/concepts/rest-api-versioning?view=azure-devops - [string] - $ApiVersion = "5.1-preview") + # The Security Namespace ID. + [Parameter(Mandatory, ValueFromPipelineByPropertyName, + ParameterSetName = 'accesscontrolentries/{NamespaceId}')] + [string] + $NamespaceID, + + # The Security Token. + [Parameter(Mandatory, ValueFromPipelineByPropertyName, + ParameterSetName = 'accesscontrolentries/{NamespaceId}')] + [string] + $SecurityToken, + + # One or more descriptors + [Parameter(ValueFromPipelineByPropertyName, + ParameterSetName = 'accesscontrolentries/{NamespaceId}')] + [string[]] + $Descriptor, + + # One or more identities. Identities will be converted into descriptors. + [Parameter(ValueFromPipelineByPropertyName)] + [string[]] + $Identity, + + # One or more allow permissions. + [Parameter(ValueFromPipelineByPropertyName)] + [string[]] + $Permission, + + # The server. By default https://dev.azure.com/. + # To use against TFS, provide the tfs server URL (e.g. http://tfsserver:8080/tfs). + [Parameter(ValueFromPipelineByPropertyName)] + [uri] + $Server = "https://dev.azure.com/", + + # The api version. By default, 5.1-preview. + # If targeting TFS, this will need to change to match your server version. + # See: https://docs.microsoft.com/en-us/azure/devops/integrate/concepts/rest-api-versioning?view=azure-devops + [string] + $ApiVersion = "5.1-preview") dynamicParam { . $GetInvokeParameters -DynamicParameter } begin { #region Copy Invoke-ADORestAPI parameters @@ -171,17 +170,21 @@ $resolveIdentity = { param( - [Parameter(Mandatory,Position=0,ValueFromPipelineByPropertyName)] - [string]$Identity) + [Parameter(Mandatory, Position = 0, ValueFromPipelineByPropertyName)] + [string]$Identity) begin { - if (-not $script:CachedIdentities) { $script:CachedIdentities = @{}} + if (-not $script:CachedIdentities) { $script:CachedIdentities = @{} } } process { + if ($ApiVersion -like '5.*' -and $sr) { + Write-Warning "The API version '$ApiVersion' may not be compatible with field operations. Consider using '7.0' or later." + } + if (-not $script:CachedIdentities[$Identity]) { $searchUri = - "https://vssps.dev.azure.com/$Organization/_apis/identities?api-version=6.0&searchfilter=General&filterValue=$Identity" + "https://vssps.dev.azure.com/$Organization/_apis/identities?api-version=6.0&searchfilter=General&filterValue=$Identity" $script:CachedIdentities[$Identity] = Invoke-ADORestAPI -Uri $searchUri } $script:CachedIdentities[$Identity] @@ -198,29 +201,30 @@ $ProgressPreference = $oldProgressPref if (-not $ProjectID) { return } } - $psBoundParameters['ParameterSet']='accesscontrolentries/{NamespaceId}' + $psBoundParameters['ParameterSet'] = 'accesscontrolentries/{NamespaceId}' switch -Regex ($psCmdlet.ParameterSetName) { Project { $null = $PSBoundParameters.Remove('ProjectID') $q.Enqueue(@{ - NamespaceID = '52d39943-cb85-4d7f-8fa8-c6baac873819' - SecurityToken = "`$PROJECT:vstfs:///Classification/TeamProject/$ProjectID" - } + $PSBoundParameters) + NamespaceID = '52d39943-cb85-4d7f-8fa8-c6baac873819' + SecurityToken = "`$PROJECT:vstfs:///Classification/TeamProject/$ProjectID" + } + $PSBoundParameters) } Analytics { $null = $PSBoundParameters.Remove('ProjectID') $q.Enqueue(@{ - NamespaceID = if ($ProjectID) { '58450c49-b02d-465a-ab12-59ae512d6531' } else { 'd34d3680-dfe5-4cc6-a949-7d9c68f73cba'} - SecurityToken = "`$/$(if ($ProjectID) { $ProjectID } else { 'Shared' })" - } + $PSBoundParameters) + NamespaceID = if ($ProjectID) { '58450c49-b02d-465a-ab12-59ae512d6531' } else { 'd34d3680-dfe5-4cc6-a949-7d9c68f73cba' } + SecurityToken = "`$/$(if ($ProjectID) { $ProjectID } else { 'Shared' })" + } + $PSBoundParameters) } 'AreaPath|IterationPath' { $gotPath = - if ($psCmdlet.ParameterSetName -eq 'AreaPath') { - Get-ADOAreaPath -Organization $Organization -Project $ProjectID -AreaPath $AreaPath - } else { - Get-ADOIterationPath -Organization $Organization -Project $ProjectID -IterationPath $iterationPath - } + if ($psCmdlet.ParameterSetName -eq 'AreaPath') { + Get-ADOAreaPath -Organization $Organization -Project $ProjectID -AreaPath $AreaPath + } + else { + Get-ADOIterationPath -Organization $Organization -Project $ProjectID -IterationPath $iterationPath + } if (-not $gotPath) { continue @@ -240,67 +244,68 @@ $null = $PSBoundParameters.Remove('ProjectID') $q.Enqueue(@{ - NamespaceID = + NamespaceID = if ($psCmdlet.ParameterSetName -eq 'AreaPath') { '83e28ad4-2d72-4ceb-97b0-c7726d5502c3' - } else { + } + else { 'bf7bfa03-b2b7-47db-8113-fa2e002cc5b1' } - SecurityToken = @(foreach($PathId in $PathIdList) { - "vstfs:///Classification/Node/$PathId" - }) -join ':' - } + $PSBoundParameters) + SecurityToken = @(foreach ($PathId in $PathIdList) { + "vstfs:///Classification/Node/$PathId" + }) -join ':' + } + $PSBoundParameters) } Dashboard { $null = $PSBoundParameters.Remove('ProjectID') $q.Enqueue(@{ - NamespaceID = '8adf73b7-389a-4276-b638-fe1653f7efc7' - SecurityToken = "$/$(if ($ProjectID) { $ProjectID })/$(if ($teamID) { $teamid } else { [guid]::Empty } )" - } + $PSBoundParameters) + NamespaceID = '8adf73b7-389a-4276-b638-fe1653f7efc7' + SecurityToken = "$/$(if ($ProjectID) { $ProjectID })/$(if ($teamID) { $teamid } else { [guid]::Empty } )" + } + $PSBoundParameters) } Plan { $q.Enqueue(@{ - NamespaceID = 'bed337f8-e5f3-4fb9-80da-81e17d06e7a8' - SecurityToken = "Plan" - } + $PSBoundParameters) + NamespaceID = 'bed337f8-e5f3-4fb9-80da-81e17d06e7a8' + SecurityToken = "Plan" + } + $PSBoundParameters) } EndpointID { $q.Enqueue(@{ - NamespaceID = '49b48001-ca20-4adc-8111-5b60c903a50c' - SecurityToken = "endpoints/$( + NamespaceID = '49b48001-ca20-4adc-8111-5b60c903a50c' + SecurityToken = "endpoints/$( if ($ProjectID) {"$ProjectID/"} else { "Collection/"} )$( if ($EndpointID) {$EndpointID} )" - } + $PSBoundParameters) + } + $PSBoundParameters) } Tagging { $q.Enqueue(@{ - NamespaceID = 'bb50f182-8e5e-40b8-bc21-e8752a1e7ae2' - SecurityToken = "/$ProjectID" - } + $PSBoundParameters) + NamespaceID = 'bb50f182-8e5e-40b8-bc21-e8752a1e7ae2' + SecurityToken = "/$ProjectID" + } + $PSBoundParameters) } ManageTFVC { $q.Enqueue(@{ - NamespaceID = 'a39371cf-0841-4c16-bbd3-276e341bc052' - SecurityToken = "/$ProjectID" - } + $PSBoundParameters) + NamespaceID = 'a39371cf-0841-4c16-bbd3-276e341bc052' + SecurityToken = "/$ProjectID" + } + $PSBoundParameters) } 'BuildDefinition|BuildPermission' { $q.Enqueue(@{ - NamespaceID = 'a39371cf-0841-4c16-bbd3-276e341bc052' - SecurityToken = "$ProjectID$(($BuildPath -replace '\\','/').TrimEnd('/'))/$DefinitionID" - } + $PSBoundParameters) + NamespaceID = 'a39371cf-0841-4c16-bbd3-276e341bc052' + SecurityToken = "$ProjectID$(($BuildPath -replace '\\','/').TrimEnd('/'))/$DefinitionID" + } + $PSBoundParameters) $q.Enqueue(@{ - NamespaceID = 'c788c23e-1b46-4162-8f5e-d7585343b5de' - SecurityToken = "$ProjectID$(($BuildPath -replace '\\','/').TrimEnd('/'))/$DefinitionID" - } + $PSBoundParameters) + NamespaceID = 'c788c23e-1b46-4162-8f5e-d7585343b5de' + SecurityToken = "$ProjectID$(($BuildPath -replace '\\','/').TrimEnd('/'))/$DefinitionID" + } + $PSBoundParameters) } 'RepositoryID|AllRepositories|ProjectRepository' { $q.Enqueue(@{ - NamespaceID = '2e9eb7ed-3c0a-47d4-87c1-0ffdd275fd87' - SecurityToken = "repoV2$( + NamespaceID = '2e9eb7ed-3c0a-47d4-87c1-0ffdd275fd87' + SecurityToken = "repoV2$( if ($ProjectID) { '/' + $projectId} )$( if ($repositoryID) {'/' + $repositoryID} @@ -310,11 +315,12 @@ if ($BranchName) { } )" - } + $PSBoundParameters) + } + $PSBoundParameters) } } - } else { - $q.Enqueue(@{ParameterSet=$ParameterSet} + $PSBoundParameters) + } + else { + $q.Enqueue(@{ParameterSet = $ParameterSet } + $PSBoundParameters) } } end { @@ -324,13 +330,13 @@ if ($BranchName) { . $DQ $q # Pop one off the queue and declare all of it's variables (see /parts/DQ.ps1). $uri = # The URI is comprised of - @( - "$server".TrimEnd('/') # the Server (minus any trailing slashes), - $Organization # the Organization, - '_apis' # the API Root ('_apis'), - (. $ReplaceRouteParameter $ParameterSet) - # and any parameterized URLs in this parameter set. - ) -as [string[]] -ne '' -join '/' + @( + "$server".TrimEnd('/') # the Server (minus any trailing slashes), + $Organization # the Organization, + '_apis' # the API Root ('_apis'), + (. $ReplaceRouteParameter $ParameterSet) + # and any parameterized URLs in this parameter set. + ) -as [string[]] -ne '' -join '/' $uri += '?' # The URI has a query string containing: $uri += @( @@ -338,18 +344,19 @@ if ($BranchName) { -not $PSBoundParameters.ApiVersion) { $ApiVersion = '2.0' } - if ($ApiVersion) { # the api-version + if ($ApiVersion) { + # the api-version "api-version=$apiVersion" } ) -join '&' $realAllow = 0 - $realDeny = 0 + $realDeny = 0 if (-not $cachedNamespaces.$namespaceID) { $cachedNamespaces.$namespaceID = - Get-ADOPermission -Organization $Organization -PersonalAccessToken $psboundParameters["PersonalAccessToken"] -PermissionType | - Where-Object NamespaceID -EQ $NamespaceID | - Select-Object -First 1 + Get-ADOPermission -Organization $Organization -PersonalAccessToken $psboundParameters["PersonalAccessToken"] -PermissionType | + Where-Object NamespaceID -EQ $NamespaceID | + Select-Object -First 1 } if (-not $cachedNamespaces.$namespaceID) { continue } @@ -369,22 +376,23 @@ if ($BranchName) { } ) - $realPermission = 0 + $realPermission = 0 $friendlyPermission = @(:nextPerm foreach ($perm in $Permission) { - if ($perm -match '^\d+$') { - $realPermission = $realPermission -bor $perm - } else { - foreach ($act in $cachedNamespaces.$namespaceID.actions) { - if ($act.Name -like $perm -or $act.DisplayName -like $perm) { - $Permission = $realPermission -bor $act.bit - $act.Name - continue nextPerm - } + if ($perm -match '^\d+$') { + $realPermission = $realPermission -bor $perm } - Write-Warning "Permission '$perm' not found in '$($cachedNamespaces.$NamespaceID.Name)'. + else { + foreach ($act in $cachedNamespaces.$namespaceID.actions) { + if ($act.Name -like $perm -or $act.DisplayName -like $perm) { + $Permission = $realPermission -bor $act.bit + $act.Name + continue nextPerm + } + } + Write-Warning "Permission '$perm' not found in '$($cachedNamespaces.$NamespaceID.Name)'. $($cachedNamespaces.$namespaceID.actions | Format-Table -Property Name, DisplayName | Out-String)" - } - }) + } + }) $c++ @@ -409,17 +417,17 @@ $($cachedNamespaces.$namespaceID.actions | Format-Table -Property Name, DisplayN ) $invokeParams.Method = 'DELETE' foreach ($desc in $Descriptors) { - Write-Progress "Removing Permissions for $desc" " (Removing: $friendlyPermission on $SecurityToken ) " -Id $progId -PercentComplete ($c * 100/$t) + Write-Progress "Removing Permissions for $desc" " (Removing: $friendlyPermission on $SecurityToken ) " -Id $progId -PercentComplete ($c * 100 / $t) if ($invokeParams.Uri -notlike "*/$realPermission") { $invokeParams.Uri += "/*/$realPermission" } $invokeParams.QueryParameter = @{ - token = $SecurityToken + token = $SecurityToken descriptor = $Descriptor } - $additionalProperties = @{Organization=$Organization;Server=$Server;SecurityToken=$SecurityToken} + $additionalProperties = @{Organization = $Organization; Server = $Server; SecurityToken = $SecurityToken } if ($WhatIfPreference) { $invokeParams continue From c13f99470ca684b7a1e7279aaeae73a89780cb70 Mon Sep 17 00:00:00 2001 From: Nico Orschel Date: Fri, 24 Oct 2025 11:25:30 +0200 Subject: [PATCH 4/6] Add warning for potential API version compatibility in Remove-ADOPermission function --- Functions/AzureDevOps/Remove-ADOPermission.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Functions/AzureDevOps/Remove-ADOPermission.ps1 b/Functions/AzureDevOps/Remove-ADOPermission.ps1 index a47742dc..03c915cf 100644 --- a/Functions/AzureDevOps/Remove-ADOPermission.ps1 +++ b/Functions/AzureDevOps/Remove-ADOPermission.ps1 @@ -178,7 +178,7 @@ } process { - if ($ApiVersion -like '5.*' -and $sr) { + if ($ApiVersion -like '5.*') { Write-Warning "The API version '$ApiVersion' may not be compatible with field operations. Consider using '7.0' or later." } From 8cf2bec4b45b44adff7e60b27c04412d788f965f Mon Sep 17 00:00:00 2001 From: Nico Orschel Date: Fri, 24 Oct 2025 11:37:10 +0200 Subject: [PATCH 5/6] Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Functions/AzureDevOps/Get-ADOPicklist.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Functions/AzureDevOps/Get-ADOPicklist.ps1 b/Functions/AzureDevOps/Get-ADOPicklist.ps1 index aad1583c..36c8d4fc 100644 --- a/Functions/AzureDevOps/Get-ADOPicklist.ps1 +++ b/Functions/AzureDevOps/Get-ADOPicklist.ps1 @@ -63,7 +63,7 @@ $q = [Collections.Queue]::new() } process { - if ($ApiVersion -like '5.*' -and $sr) { + if ($ApiVersion -like '5.*') { Write-Warning "The API version '$ApiVersion' may not be compatible with field operations. Consider using '7.0' or later." } From 523d7f11069c4b7d1a522d05cb780b56afb8eef0 Mon Sep 17 00:00:00 2001 From: Nico Orschel Date: Fri, 24 Oct 2025 11:39:40 +0200 Subject: [PATCH 6/6] Refactor parameter formatting and add API version compatibility warning in Remove-ADOPicklist function --- Functions/AzureDevOps/Remove-ADOPicklist.ps1 | 110 ++++++++++--------- 1 file changed, 58 insertions(+), 52 deletions(-) diff --git a/Functions/AzureDevOps/Remove-ADOPicklist.ps1 b/Functions/AzureDevOps/Remove-ADOPicklist.ps1 index afb547d1..45592932 100644 --- a/Functions/AzureDevOps/Remove-ADOPicklist.ps1 +++ b/Functions/AzureDevOps/Remove-ADOPicklist.ps1 @@ -11,40 +11,40 @@ .Link https://docs.microsoft.com/en-us/rest/api/azure/devops/processes/lists/delete #> - [CmdletBinding(SupportsShouldProcess,ConfirmImpact='High')] + [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')] [OutputType([Nullable], [Hashtable])] param( - # The Organization. - [Parameter(Mandatory,ValueFromPipelineByPropertyName)] - [Alias('Org')] - [string] - $Organization, - - # The PicklistID. - [Parameter(Mandatory,ValueFromPipelineByPropertyName, - ParameterSetName='work/processes/lists/{PicklistId}')] - [string] - $PicklistID, - - # A list of items to remove. - # If this parameter is provided, the picklist items will be removed, and the picklist will not be deleted. - # If this parameter is not provided, the picklist will not be deleted. - [Parameter(ParameterSetName='work/processes/lists/{PicklistId}')] - [Alias('Value', 'Items','Values')] - [string[]] - $Item, - - # The server. By default https://dev.azure.com/. - # To use against TFS, provide the tfs server URL (e.g. http://tfsserver:8080/tfs). - [Parameter(ValueFromPipelineByPropertyName)] - [uri] - $Server = "https://dev.azure.com/", - - # The api version. By default, 5.1-preview. - # If targeting TFS, this will need to change to match your server version. - # See: https://docs.microsoft.com/en-us/azure/devops/integrate/concepts/rest-api-versioning?view=azure-devops - [string] - $ApiVersion = "5.1-preview" + # The Organization. + [Parameter(Mandatory, ValueFromPipelineByPropertyName)] + [Alias('Org')] + [string] + $Organization, + + # The PicklistID. + [Parameter(Mandatory, ValueFromPipelineByPropertyName, + ParameterSetName = 'work/processes/lists/{PicklistId}')] + [string] + $PicklistID, + + # A list of items to remove. + # If this parameter is provided, the picklist items will be removed, and the picklist will not be deleted. + # If this parameter is not provided, the picklist will not be deleted. + [Parameter(ParameterSetName = 'work/processes/lists/{PicklistId}')] + [Alias('Value', 'Items', 'Values')] + [string[]] + $Item, + + # The server. By default https://dev.azure.com/. + # To use against TFS, provide the tfs server URL (e.g. http://tfsserver:8080/tfs). + [Parameter(ValueFromPipelineByPropertyName)] + [uri] + $Server = "https://dev.azure.com/", + + # The api version. By default, 5.1-preview. + # If targeting TFS, this will need to change to match your server version. + # See: https://docs.microsoft.com/en-us/azure/devops/integrate/concepts/rest-api-versioning?view=azure-devops + [string] + $ApiVersion = "5.1-preview" ) dynamicParam { . $GetInvokeParameters -DynamicParameter } @@ -56,8 +56,12 @@ } process { + if ($ApiVersion -like '5.*') { + Write-Warning "The API version '$ApiVersion' may not be compatible with field operations. Consider using '7.0' or later." + } + $ParameterSet = $psCmdlet.ParameterSetName - $q.Enqueue(@{ParameterSet=$ParameterSet} + $PSBoundParameters) + $q.Enqueue(@{ParameterSet = $ParameterSet } + $PSBoundParameters) } end { $c, $t, $id = 0, $q.Count, [Random]::new().Next() @@ -67,13 +71,13 @@ $uri = # The URI is comprised of: - @( - "$server".TrimEnd('/') # the Server (minus any trailing slashes), - $Organization # the Organization, - '_apis' # the API Root ('_apis'), - (. $ReplaceRouteParameter $ParameterSet) - # and any parameterized URLs in this parameter set. - ) -as [string[]] -ne '' -join '/' + @( + "$server".TrimEnd('/') # the Server (minus any trailing slashes), + $Organization # the Organization, + '_apis' # the API Root ('_apis'), + (. $ReplaceRouteParameter $ParameterSet) + # and any parameterized URLs in this parameter set. + ) -as [string[]] -ne '' -join '/' $uri += '?' # The URI has a query string containing: $uri += @( @@ -81,13 +85,14 @@ -not $PSBoundParameters.ApiVersion) { $ApiVersion = '2.0' } - if ($ApiVersion) { # the api-version + if ($ApiVersion) { + # the api-version "api-version=$apiVersion" } ) -join '&' $c++ - Write-Progress "Removing $($Item -join ' ')" "[$c/$t] $uri" -Id $id -PercentComplete ($c * 100/$t) + Write-Progress "Removing $($Item -join ' ')" "[$c/$t] $uri" -Id $id -PercentComplete ($c * 100 / $t) $invokeParams.Uri = $uri @@ -98,25 +103,26 @@ $getPicklistSplat.Remove('Name') $getPicklistSplat.Remove('Item') $picklistItems = Get-ADOPicklist @getPicklistSplat - $picklistItems.items = @( + $picklistItems.items = @( $picklistItems.items | - Where-Object { - foreach ($i in $item) { - if ( $_ -like $i) { return } - } - $_ - }) + Where-Object { + foreach ($i in $item) { + if ( $_ -like $i) { return } + } + $_ + }) $invokeParams.body = $picklistItems $invokeParams.pstypename = "$Organization.Picklist.Detail", "PSDevOps.Picklist.Detail" - } else { - $invokeParams.Method = 'DELETE' + } + else { + $invokeParams.Method = 'DELETE' } if ($WhatIfPreference) { $invokeParams.Remove('PersonalAccessToken') $invokeParams continue } - if (-not $psCmdlet.ShouldProcess("$($invokeParams.Method) $($item -join ' ') $($invokeParams.uri)")) {continue } + if (-not $psCmdlet.ShouldProcess("$($invokeParams.Method) $($item -join ' ') $($invokeParams.uri)")) { continue } Invoke-ADORestAPI @invokeParams }