Last active
April 17, 2024 14:45
-
-
Save rfennell/3f4dc4c3ac6cb5d5f1d2032211e1c0ae to your computer and use it in GitHub Desktop.
A PowerShell script to revert the values in a specified list of fields for a list of work items returned by a WIQL query
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
[CmdletBinding()] | |
<# | |
.SYNOPSIS | |
Reverts work items to their previous state in Azure DevOps based on the specified criteria. | |
.DESCRIPTION | |
Reverts work items to their previous state in Azure DevOps based on the specified criteria. | |
.PARAMETER pat | |
The Personal Access Token (PAT) used to authenticate with Azure DevOps. | |
.PARAMETER organizationUrl | |
The URL of the Azure DevOps organization | |
.PARAMETER projectName | |
The name of the Azure DevOps project. | |
.PARAMETER changedBy | |
The name of the user who made the changes to the work items. | |
.PARAMETER logFile | |
(Optional) The path to the log file where the script will write the results. | |
.PARAMETER whatif | |
(Optional) If the -whatif switch is provided, the script will only test the process without making any changes. | |
.EXAMPLE | |
.\revert-workitems.ps1 -pat "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" -organizationUrl "https://dev.azure.com/contoso" -projectName "MyProject" -changedBy "[email protected]" -logFile "C:\Logs\revert.log" | |
This example reverts work items in the "MyProject" Azure DevOps project that were changed by the user "[email protected]". The script will authenticate using the provided PAT and organization URL, and write the results to the specified log file. | |
#> | |
param ( | |
[Parameter(Mandatory = $true)] | |
$pat, | |
[Parameter(Mandatory = $true)] | |
$organizationUrl , | |
[Parameter(Mandatory = $true)] | |
$projectName , | |
[Parameter(Mandatory = $true)] | |
$changedBy, | |
[Parameter(Mandatory = $false)] | |
$logFile = "", | |
[Parameter(Mandatory = $false)] | |
[switch]$whatif # if -whatif is provided, will only test the process | |
) | |
function Revert-WorkItem { | |
[CmdletBinding()] | |
param ( | |
$pat, | |
$organizationUrl , | |
$projectName , | |
$workItemId, | |
$fieldsofInterest = @( | |
"System.WorkItemType", | |
"System.Title", | |
"System.Description", | |
"Microsoft.VSTS.Common.AcceptanceCriteria", | |
"System.AssignedTo", | |
"System.State", | |
"Custom.DeployedStatus", | |
"System.Tags", | |
"System.IterationPath", | |
"System.AreaPath", | |
"Microsoft.VSTS.Common.Severity", | |
"Microsoft.VSTS.Common.Resolution", | |
"Custom.RCACategory", | |
"Custom.RCADevelopment"), | |
[switch]$whatif | |
) | |
# Import necessary modules | |
Import-Module -Name PowerShellGet | |
$token = [System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes(":$($pat)")) | |
# Define headers for HTTP request | |
$headers = @{ | |
"Authorization" = "Basic $token" | |
"Content-Type" = "application/json-patch+json" | |
} | |
Write-Host "Getting work item $workItemId..." -ForegroundColor green | |
# Fetch work item current state | |
$currentState = Invoke-RestMethod -Uri "$organizationUrl/$projectName/_apis/wit/workitems/$workItemId`?api-version=6.0" -Method Get -Headers $headers | |
# Fetch work item last update | |
$updates = Invoke-RestMethod -Uri "$organizationUrl/$projectName/_apis/wit/workitems/$workItemId/updates`?api-version=6.0" -Method Get -Headers $headers | |
# you can't trust the rev to be the update number, so can't use /updates/rev. Have to get the last one re | |
$lastupdate = $updates.value[-1] | |
if ($updates.count -lt 2) { | |
Write-host "- SKIP - WI has not been updated since initial creation" -ForegroundColor yellow | |
return 1 | |
} | |
elseif ($lastupdate.revisedBy.uniqueName.tolower() -ne $changedBy.tolower()) { | |
Write-host "- SKIP - As WI has been update by $($lastupdate.revisedBy.displayName) since updated by $changedBy" -ForegroundColor yellow | |
return 0 | |
} else { | |
Write-host "- Last update by: $($lastupdate.revisedBy.displayName)" -ForegroundColor green | |
Write-host "- Fields of Interest updated: " -ForegroundColor green | |
# build the payload, starting with revision counter which must be present and correct | |
$payload = @() | |
$payload += @{ | |
op = "test" | |
path = "/rev" | |
value = $currentState.rev | |
} | |
# iterate arosss the field property and list the changes | |
foreach ($field in $lastupdate.fields.PSObject.Properties.Name) { | |
if ($field -notin $fieldsofInterest) { | |
continue | |
} | |
foreach ($change in $lastupdate.fields.$field) { | |
Write-Host "-- $field $($change.oldValue) -> $($change.newValue)" -ForegroundColor green | |
# some fields if being cleared required extra processing | |
if ($lastupdate.fields.$field.oldValue -ne $null) { | |
$value = $lastupdate.fields.$field.oldValue | |
} | |
else { | |
$value = '' | |
} | |
$payload += @{ | |
op = "replace" | |
path = "/fields/$field" | |
value = $value | |
} | |
} | |
} | |
if ($payload.Count -eq 1) { | |
Write-Host "- No changes to revert" -ForegroundColor Yellow | |
return 1 | |
} | |
else { | |
# write the WI back to the server | |
$json = $payload | ConvertTo-Json -Depth 10 | |
Write-Verbose $json | |
if ($whatif.IsPresent) { | |
Write-Host "Would reverting work item $workItemId... (dry-run validate only)" -ForegroundColor Yellow | |
$payload | ForEach-Object { | |
write-host "- $($_.path) : $($_.value)" -ForegroundColor Yellow | |
} | |
# run the rest call in test mode | |
try { | |
Invoke-RestMethod -Uri "$organizationUrl/$projectName/_apis/wit/workitems/$workItemId`?validateOnly=true&api-version=6.0" -Method Patch -Headers $headers -Body $json | |
} catch { | |
Write-host "Error validating update request with Azure DevOps $($_.ErrorDetails.Message)" -ForegroundColor red | |
return -1 | |
} | |
} | |
else { | |
Write-Host "Reverting work item $workItemId..." -ForegroundColor green | |
try { | |
Invoke-RestMethod -Uri "$organizationUrl/$projectName/_apis/wit/workitems/$workItemId`?api-version=6.0" -Method Patch -Headers $headers -Body $json | |
} catch { | |
Write-host "Error updating request on Azure DevOps $($_.ErrorDetails.Message)" -ForegroundColor red | |
return -1 | |
} | |
} | |
return 2 | |
} | |
} | |
} | |
function Get-WorkItems { | |
[CmdletBinding()] | |
param ( | |
$pat, | |
$organizationUrl , | |
$projectName, | |
$wiql | |
) | |
# Import necessary modules | |
Import-Module -Name PowerShellGet | |
$token = [System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes(":$($pat)")) | |
# Define headers for HTTP request | |
$headers = @{ | |
"Authorization" = "Basic $token" | |
"Content-Type" = "application/json" | |
} | |
$json = @{query = $wiql } | ConvertTo-Json -Depth 10 | |
Write-Verbose $json | |
# build a list of workitems based on a wiql query | |
$response = Invoke-RestMethod -Uri "$organizationUrl/$projectName/_apis/wit/wiql`?api-version=6.0" -Method Post -Headers $headers -Body $json | |
$response | Select-Object -ExpandProperty workItems | Select-Object -ExpandProperty id | |
} | |
write-host "Searching for work items to revert that were edited tody by $changedBy " -ForegroundColor green | |
$workitems = Get-WorkItems -pat $pat -organizationUrl $organizationUrl -projectName $projectName -wiql "SELECT [System.Id] FROM WorkItems WHERE [System.TeamProject] = '$projectName' AND [System.ChangedDate] = @today AND [System.ChangedBy] = '$changedBy'" | |
# Delete $logFile if present | |
if ($logFile.Length -gt 0) { | |
if (Test-Path $logFile) { | |
Write-host "Deleting old logfile $logfile" | |
Remove-Item $logFile -Force | |
} | |
} | |
$results = @{} | |
foreach ($id in $workitems) { | |
$return = Revert-WorkItem -pat $pat -organizationUrl $organizationUrl -projectName $projectName -workItemId $id -whatif:$($whatif.IsPresent) | |
switch ($return) { | |
-1 { $results[$id] = "Errored" } | |
1 { $results[$id] = "Not Reverted" } | |
2 { $results[$id] = "Reverted" } | |
Default { $results[$id] = "Skipped" } | |
} | |
if ($logFile.Length -gt 0) { | |
# Append the value of $return to a CSV file | |
$results | Select-Object @{Name = "WorkItemId"; Expression = { $id } }, @{Name = "Result"; Expression = { $results[$id] } } | Export-Csv -Path $logFile -Append -NoTypeInformation | |
} | |
} | |
Write-host "Completed - Found $($workitems.Count) work items" | |
write-host "- Reverted $(($results.GetEnumerator() | ?{$_.Value -eq "Reverted"}).Count) as last edit was by $changedBy" | |
write-host "- Skipped $(($results.GetEnumerator() | ?{$_.Value -eq "Not Reverted"}).Count) as no tracked fields were updated in last edit by $changedBy" | |
write-host "- Skipped $(($results.GetEnumerator() | ?{$_.Value -eq "Skipped"}).Count) as last edit was not by $changedBy" | |
write-host "- Errored $(($results.GetEnumerator() | ?{$_.Value -eq "Errored"}).Count) as could not save updated Work Item" | |
if ($logFile.Length -gt 0) { | |
write-host "Details of WI updates written to $logfile" | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment