Public/DataImportTool/Remove-WitRuleScope.ps1
|
function Remove-WitRuleScope { <# .SYNOPSIS Removes for=/not= identity scoping from the rules and workflow transitions of a work item type. .DESCRIPTION Azure DevOps Services cannot resolve identity scoping during ProcessValidation: a single for= or not= attribute anywhere in a type definition fails the import with the bare message "Invalid process template: <Type>.xml:: Object reference not set to an instance of an object." Both field rules (VS237302) and workflow TRANSITION elements are affected. Pass -Identity with -AttributeName to strip one specific scope, or -All to strip every for= and not= in the type in a single export/import round trip. -All is the right choice when clearing a type for import, and avoids re-importing the same type once per identity. Removing scope from a TRANSITION is a behaviour change: a state change that was restricted to a group becomes available to everyone with write access. .EXAMPLE Remove-WitRuleScope -Project 'Mammoth' -WorkItemType 'Release' -AttributeName 'both' -Identity '[global]\Project Collection Administrators' .EXAMPLE Remove-WitRuleScope -Project 'Whistler' -WorkItemType 'Bug' -All #> [CmdletBinding(DefaultParameterSetName = 'Identity')] param( [Parameter(Mandatory)] [string]$Collection, [Parameter(Mandatory)] [string]$Project, [Parameter(Mandatory)] [string]$WorkItemType, [Parameter(Mandatory, ParameterSetName = 'Identity')] [ValidateSet('for', 'not', 'both')] [string]$AttributeName, [Parameter(Mandatory, ParameterSetName = 'Identity')] [string]$Identity, [Parameter(Mandatory, ParameterSetName = 'All')] [switch]$All, [string]$WitAdminPath ) if ($All) { Write-FixStep "Removing ALL rule and transition scoping from work item type '$WorkItemType' in '$Project'" $attributes = @('for', 'not') } else { Write-FixStep "Removing rule scope '$Identity' ($AttributeName) from work item type '$WorkItemType' in '$Project'" $attributes = if ($AttributeName -eq 'both') { @('for', 'not') } else { @($AttributeName) } } $file = Join-Path ([System.IO.Path]::GetTempPath()) "$([guid]::NewGuid()).Witd.xml" try { Invoke-WitAdminFix -WitAdminPath $WitAdminPath -Arguments @('exportwitd', "/collection:$Collection", "/p:$Project", "/n:$WorkItemType", "/f:$file") $xml = [xml](Get-Content -LiteralPath $file -Raw) $changeCount = 0 foreach ($attribute in $attributes) { $nodes = @($xml.SelectNodes("//*[@$attribute]")) if (-not $All) { $nodes = @($nodes | Where-Object { $_.GetAttribute($attribute) -eq $Identity }) } foreach ($node in $nodes) { # A TRANSITION is identified by its from/to states; a field rule by the # refname of the FIELD it sits under. Log whichever applies. $context = if ($node.LocalName -eq 'TRANSITION') { "'$($node.GetAttribute('from'))' -> '$($node.GetAttribute('to'))'" } else { $field = $node.SelectSingleNode('ancestor-or-self::*[@refname]') if ($field) { "field '$($field.GetAttribute('refname'))'" } else { "under $($node.ParentNode.LocalName)" } } Write-FixStep " removing @$attribute='$($node.GetAttribute($attribute))' from <$($node.LocalName)> $context" [void]$node.RemoveAttribute($attribute) $changeCount++ } } if ($changeCount -eq 0) { if ($All) { Write-FixStep " '$WorkItemType' has no scoped rules or transitions - no change." return } $scopes = @($xml.SelectNodes('//*[@for or @not]') | ForEach-Object { "<$($_.LocalName)> for='$($_.GetAttribute('for'))' not='$($_.GetAttribute('not'))'" } | Sort-Object -Unique) $detail = if ($scopes) { " Scoped rules present: $($scopes -join '; ')" } else { ' No scoped rules exist in this work item type.' } Write-FixStep " no matching scope for '$Identity' in '$WorkItemType' - no change.$detail" return } Write-FixStep " removed $changeCount scope attribute(s)" $xml.Save($file) Invoke-WitAdminFix -WitAdminPath $WitAdminPath -Arguments @('importwitd', "/collection:$Collection", "/p:$Project", "/f:$file") # Verify the server actually took the change - witadmin has been observed # reporting success while the import did not stick. Invoke-WitAdminFix -WitAdminPath $WitAdminPath -Arguments @('exportwitd', "/collection:$Collection", "/p:$Project", "/n:$WorkItemType", "/f:$file") $verify = [xml](Get-Content -LiteralPath $file -Raw) $remaining = if ($All) { @($verify.SelectNodes('//*[@for or @not]')) } else { @(foreach ($attribute in $attributes) { $verify.SelectNodes("//*[@$attribute]") | Where-Object { $_.GetAttribute($attribute) -eq $Identity } }) } if ($remaining.Count -gt 0) { $what = if ($All) { 'scope attribute(s)' } else { "scope(s) for '$Identity'" } throw "Verification failed: $($remaining.Count) $what still present in '$WorkItemType' after import." } Write-FixStep " verified: no $(if ($All) { 'scoping' } else { "'$Identity' scoping" }) remains in '$WorkItemType'" } finally { Remove-Item -LiteralPath $file -Force -ErrorAction SilentlyContinue } } |