Public/Update-M365PTaskSmart.ps1
|
function Update-M365PTaskSmart { <# .SYNOPSIS Updates a Planner task with automatic ETag handling and concurrency control. .DESCRIPTION This function provides a safe way to update Planner tasks by automatically handling ETag concurrency control. It retrieves the current ETag before updating and implements retry logic for 412 Precondition Failed errors caused by concurrent modifications. .PARAMETER TaskId The ID of the task to update. .PARAMETER Title Optional. New title for the task. .PARAMETER BucketId Optional. Move the task to a different bucket by specifying the new bucket ID. .PARAMETER PercentComplete Optional. Update the completion percentage (0-100). .PARAMETER StartDateTime Optional. Set or update the task start date. .PARAMETER DueDateTime Optional. Set or update the task due date. .PARAMETER Priority Optional. Set task priority (0-10, where 0 is urgent and 10 is low priority). .PARAMETER MaxRetries Optional. Maximum number of retry attempts for 412 errors. Default is 1. .EXAMPLE Update-M365PTaskSmart -TaskId "task123" -Title "Updated Task Title" Updates only the task title with automatic ETag handling. .EXAMPLE Update-M365PTaskSmart -TaskId "task123" -PercentComplete 50 -Priority 5 Updates multiple properties of the task. .EXAMPLE Get-MgPlannerPlanTask -PlannerPlanId "plan123" | Where-Object {$_.Title -like "*Review*"} | ForEach-Object { Update-M365PTaskSmart -TaskId $_.Id -PercentComplete 100 } Marks all tasks containing "Review" as complete using pipeline input. .EXAMPLE Update-M365PTaskSmart -TaskId "task123" -DueDateTime "2026-03-01" -MaxRetries 3 Updates the due date with up to 3 retry attempts on concurrency conflicts. .NOTES Requires Microsoft.Graph.Planner module and appropriate permissions: - Tasks.ReadWrite This function implements automatic retry logic for ETag conflicts (HTTP 412). The ETag is automatically retrieved and passed to the update operation. #> [CmdletBinding(SupportsShouldProcess = $true)] param( [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [Alias('Id')] [string]$TaskId, [Parameter(Mandatory = $false)] [string]$Title, [Parameter(Mandatory = $false)] [string]$BucketId, [Parameter(Mandatory = $false)] [ValidateRange(0, 100)] [int]$PercentComplete, [Parameter(Mandatory = $false)] [datetime]$StartDateTime, [Parameter(Mandatory = $false)] [datetime]$DueDateTime, [Parameter(Mandatory = $false)] [ValidateRange(0, 10)] [int]$Priority, [Parameter(Mandatory = $false)] [ValidateRange(0, 5)] [int]$MaxRetries = 1 ) begin { Write-Verbose "Starting Update-M365PTaskSmart" # Function to perform the actual update with ETag function Invoke-TaskUpdate { param( [string]$TaskId, [hashtable]$UpdateParams, [int]$AttemptNumber ) try { # Get current task to retrieve ETag Write-Verbose "Attempt $AttemptNumber : Retrieving current task state for ETag..." $currentTask = Get-MgPlannerTask -PlannerTaskId $TaskId if (-not $currentTask) { throw "Task not found: $TaskId" } $etag = $currentTask.AdditionalProperties['@odata.etag'] Write-Verbose "Current ETag: $etag" # Add ETag to update parameters $UpdateParams['IfMatch'] = $etag $UpdateParams['PlannerTaskId'] = $TaskId # Perform the update Write-Verbose "Executing update operation..." $updatedTask = Update-MgPlannerTask @UpdateParams Write-Verbose "Task updated successfully" return @{ Success = $true Task = $updatedTask Attempts = $AttemptNumber } } catch { # Check if this is a 412 Precondition Failed error if ($_.Exception.Message -match '412|Precondition Failed|etag') { Write-Verbose "ETag conflict detected (412 Precondition Failed)" return @{ Success = $false Error = $_ IsETagConflict = $true Attempts = $AttemptNumber } } else { # Different error, don't retry Write-Verbose "Non-ETag error occurred: $($_.Exception.Message)" return @{ Success = $false Error = $_ IsETagConflict = $false Attempts = $AttemptNumber } } } } } process { try { Write-Verbose "Processing task: $TaskId" # Build update parameters hashtable $updateParams = @{} if ($PSBoundParameters.ContainsKey('Title')) { $updateParams['Title'] = $Title } if ($PSBoundParameters.ContainsKey('BucketId')) { $updateParams['BucketId'] = $BucketId } if ($PSBoundParameters.ContainsKey('PercentComplete')) { $updateParams['PercentComplete'] = $PercentComplete } if ($PSBoundParameters.ContainsKey('StartDateTime')) { $updateParams['StartDateTime'] = $StartDateTime.ToString('yyyy-MM-ddTHH:mm:ssZ') } if ($PSBoundParameters.ContainsKey('DueDateTime')) { $updateParams['DueDateTime'] = $DueDateTime.ToString('yyyy-MM-ddTHH:mm:ssZ') } if ($PSBoundParameters.ContainsKey('Priority')) { $updateParams['Priority'] = $Priority } # Check if there are any parameters to update if ($updateParams.Count -eq 0) { Write-Warning "No update parameters specified for task: $TaskId" return } Write-Verbose "Update parameters: $($updateParams.Keys -join ', ')" # WhatIf support if ($PSCmdlet.ShouldProcess("Task $TaskId", "Update with parameters: $($updateParams.Keys -join ', ')")) { # Attempt update with retry logic $attemptNumber = 1 $result = $null do { Write-Verbose "Update attempt $attemptNumber of $($MaxRetries + 1)" $result = Invoke-TaskUpdate -TaskId $TaskId -UpdateParams $updateParams -AttemptNumber $attemptNumber if ($result.Success) { # Success! Write-Verbose "Task updated successfully on attempt $($result.Attempts)" return [PSCustomObject]@{ TaskId = $TaskId Status = 'Success' Attempts = $result.Attempts UpdatedProperties = $updateParams.Keys } } elseif ($result.IsETagConflict -and $attemptNumber -le $MaxRetries) { # ETag conflict and we have retries left Write-Warning "ETag conflict on attempt $attemptNumber. Retrying..." Start-Sleep -Milliseconds (500 * $attemptNumber) # Exponential backoff $attemptNumber++ } else { # Either not an ETag conflict, or we're out of retries if ($result.IsETagConflict) { $errorMessage = "Failed to update task after $($result.Attempts) attempts due to repeated ETag conflicts. The task is being modified by another process." } else { $errorMessage = "Failed to update task: $($result.Error.Exception.Message)" } Write-Error $errorMessage return [PSCustomObject]@{ TaskId = $TaskId Status = 'Failed' Attempts = $result.Attempts Error = $errorMessage } } } while ($attemptNumber -le ($MaxRetries + 1)) } } catch { Write-Error "Unexpected error updating task $TaskId : $_" throw } } end { Write-Verbose "Update-M365PTaskSmart completed" } } |