Public/Get-AzureLocalUpdateRuns.ps1
|
function Get-AzureLocalUpdateRuns { <# .SYNOPSIS Gets update run history and status for one or more Azure Local clusters. .DESCRIPTION Retrieves update run information for Azure Local (Azure Stack HCI) clusters. Update runs contain the history and status of update operations including start time, end time, progress, and any errors that occurred. Supports multiple input methods: - Single cluster by name (original behavior) - Multiple clusters by name or resource ID - All clusters matching an UpdateRing tag value Returns clean, human-readable objects with key information extracted from the API response. .PARAMETER ClusterName The name of a single Azure Local cluster (original behavior). .PARAMETER ClusterNames An array of Azure Local cluster names to query. .PARAMETER ClusterResourceIds An array of full Azure Resource IDs for the clusters to query. .PARAMETER ScopeByUpdateRingTag When specified, finds clusters by the 'UpdateRing' tag via Azure Resource Graph. Must be used together with -UpdateRingValue. .PARAMETER UpdateRingValue The value of the 'UpdateRing' tag to match when using -ScopeByUpdateRingTag. .PARAMETER ResourceGroupName The resource group containing the cluster. If not specified, searches all resource groups. .PARAMETER SubscriptionId The Azure subscription ID. If not specified, uses the current subscription context. .PARAMETER UpdateName Optional. The specific update name to get runs for. If not specified, returns runs for all updates. .PARAMETER Latest Optional. Return only the most recent update run per cluster. .PARAMETER Raw Optional. Return the raw API response objects instead of formatted output. .PARAMETER ApiVersion The Azure REST API version to use. Default is the module's default API version. .PARAMETER ExportPath Path to export the results. Supports .csv, .json, and .xml (JUnit format) extensions. .OUTPUTS PSCustomObject[] - Array of update run objects with the following properties: - ClusterName: The cluster name (in multi-cluster mode) - UpdateName: The update package name (e.g., "Solution12.2601.1002.38") - RunId: The unique GUID for this update run - State: Current state (InProgress, Succeeded, Failed, etc.) - StartTime: When the update run started - Duration: How long the update has been running or took to complete - Progress: Step completion progress (e.g., "3/5 steps") - CurrentStep: The currently executing or failed step name - Location: Azure region .EXAMPLE # Single cluster (original behavior) Get-AzureLocalUpdateRuns -ClusterName "MyCluster" -ResourceGroupName "MyRG" .EXAMPLE # Multiple clusters by tag Get-AzureLocalUpdateRuns -ScopeByUpdateRingTag -UpdateRingValue "Wave1" -Latest .EXAMPLE # Export to CSV Get-AzureLocalUpdateRuns -ScopeByUpdateRingTag -UpdateRingValue "Production" -Latest -ExportPath "C:\Reports\runs.csv" .EXAMPLE Get-AzureLocalUpdateRuns -ClusterName "MyCluster" -Raw Gets raw API response for programmatic processing. #> [CmdletBinding(DefaultParameterSetName = 'SingleCluster')] [OutputType([PSCustomObject[]])] param( [Parameter(Mandatory = $true, ParameterSetName = 'SingleCluster')] [string]$ClusterName, [Parameter(Mandatory = $true, ParameterSetName = 'ByName')] [string[]]$ClusterNames, [Parameter(Mandatory = $true, ParameterSetName = 'ByResourceId')] [string[]]$ClusterResourceIds, [Parameter(Mandatory = $true, ParameterSetName = 'ByTag')] [switch]$ScopeByUpdateRingTag, [ValidatePattern('^[A-Za-z0-9_-]{1,64}$')] [Parameter(Mandatory = $true, ParameterSetName = 'ByTag')] [string]$UpdateRingValue, [Parameter(Mandatory = $false, ParameterSetName = 'SingleCluster')] [Parameter(Mandatory = $false, ParameterSetName = 'ByName')] [string]$ResourceGroupName, [Parameter(Mandatory = $false)] [string]$SubscriptionId, [Parameter(Mandatory = $false)] [string]$UpdateName, [Parameter(Mandatory = $false)] [switch]$Latest, [Parameter(Mandatory = $false)] [switch]$Raw, [Parameter(Mandatory = $false)] [string]$ApiVersion = $script:DefaultApiVersion, [Parameter(Mandatory = $false, ParameterSetName = 'ByName')] [Parameter(Mandatory = $false, ParameterSetName = 'ByResourceId')] [Parameter(Mandatory = $false, ParameterSetName = 'ByTag')] [string]$ExportPath, [Parameter(Mandatory = $false, ParameterSetName = 'ByName')] [Parameter(Mandatory = $false, ParameterSetName = 'ByResourceId')] [Parameter(Mandatory = $false, ParameterSetName = 'ByTag')] [ValidateSet('Auto', 'Csv', 'Json', 'JUnitXml')] [string]$ExportFormat = 'Auto', [Parameter(Mandatory = $false)] [switch]$PassThru, [Parameter(Mandatory = $false, ParameterSetName = 'ByName')] [Parameter(Mandatory = $false, ParameterSetName = 'ByResourceId')] [Parameter(Mandatory = $false, ParameterSetName = 'ByTag')] [ValidateRange(1, 32)] [int]$ThrottleLimit = 1, # v0.7.1: when omitted (default), Get-AzureLocalUpdateRuns will auto-reset # the UpdateSideloaded tag (True->False) and clear UpdateVersionInProgress # for any cluster whose latest update run is Succeeded AND whose # UpdateVersionInProgress tag matches the run's update name. Pass this # switch on read-only audit pipelines that must not mutate cluster tags. [Parameter(Mandatory = $false)] [switch]$SkipSideloadedReset ) # Pre-flight: Validate export path is writable before expensive operations if ($ExportPath) { try { Test-ExportPathWritable -Path $ExportPath | Out-Null } catch { Write-Warning $_.Exception.Message; return } } # Original single-cluster behavior if ($PSCmdlet.ParameterSetName -eq 'SingleCluster') { Test-AzCliAvailable | Out-Null Write-Log -Message "" -Level Info Write-Log -Message "========================================" -Level Header Write-Log -Message "Azure Local Cluster Update Runs" -Level Header Write-Log -Message "========================================" -Level Header Write-Log -Message "Cluster: $ClusterName" -Level Info if (-not $SubscriptionId) { $SubscriptionId = (az account show --query id -o tsv) Write-Log -Message "Using current subscription: $SubscriptionId" -Level Info } Write-Log -Message "Looking up cluster resource..." -Level Info $clusterInfo = Get-AzureLocalClusterInfo -ClusterName $ClusterName ` -ResourceGroupName $ResourceGroupName ` -SubscriptionId $SubscriptionId ` -ApiVersion $ApiVersion if (-not $clusterInfo) { Write-Log -Message "Cluster '$ClusterName' not found." -Level Error return $null } Write-Log -Message "Found cluster: $($clusterInfo.id)" -Level Success Write-Log -Message "Querying update runs..." -Level Info $allRuns = Get-AzLocalClusterUpdateRuns -resourceId $clusterInfo.id -updateNameFilter $UpdateName -apiVer $ApiVersion Write-Log -Message "Found $($allRuns.Count) update run(s)" -Level $(if ($allRuns.Count -gt 0) { "Success" } else { "Warning" }) if ($Raw) { if ($Latest) { return $allRuns | Sort-Object { $_.properties.timeStarted } -Descending | Select-Object -First 1 } return $allRuns } # Format runs $formattedRuns = [System.Collections.Generic.List[object]]::new() foreach ($run in $allRuns) { $formattedRuns.Add((Format-AzLocalUpdateRun -run $run -clusterName $ClusterName -clusterResourceId $clusterInfo.id)) | Out-Null } $formattedRuns = @($formattedRuns | Sort-Object StartTime -Descending) if ($Latest) { $formattedRuns = @($formattedRuns | Select-Object -First 1) } if ($formattedRuns.Count -gt 0) { Write-Log -Message "" -Level Info Write-Log -Message "Update Runs for Cluster: $ClusterName" -Level Header Write-Log -Message ("=" * 60) -Level Header $formattedRuns | Format-Table -AutoSize | Out-String | Write-Host # If the latest run failed due to health check, show blocking health failures $latestRun = $formattedRuns | Select-Object -First 1 if ($latestRun.State -eq "Failed" -and $latestRun.CurrentStep -match "health check") { Write-Log -Message "The latest update run was blocked by health check failures." -Level Warning Write-Log -Message "Querying current health check status..." -Level Info $healthResults = Test-AzureLocalClusterHealth -ClusterResourceIds @($clusterInfo.id) -BlockingOnly if ($healthResults -and $healthResults[0].CriticalCount -gt 0) { Write-Log -Message "" -Level Info Write-Log -Message "The following critical health issues must be resolved before this update can proceed:" -Level Error foreach ($failure in $healthResults[0].Failures) { $nodeInfo = if ($failure.TargetResourceName) { " (Node: $($failure.TargetResourceName))" } else { "" } Write-Log -Message " [Critical] $($failure.CheckName)$nodeInfo`: $($failure.Description)" -Level Error if ($failure.Remediation) { Write-Log -Message " Remediation: $($failure.Remediation)" -Level Warning } } } } } else { Write-Log -Message "" -Level Info Write-Log -Message "No update runs found for cluster '$ClusterName'" -Level Warning } # Display latest run details if ($formattedRuns.Count -gt 0) { Write-Log -Message "" -Level Info Write-Log -Message "Latest Update Run:" -Level Header Write-Host "" $formattedRuns | Select-Object -First 1 | Format-List | Out-String -Stream | ForEach-Object { if ($_ -ne "") { Write-Host "`t$_" } } Write-Host "" } # v0.7.1: Sideloaded auto-reset (default ON; -SkipSideloadedReset to disable). if (-not $SkipSideloadedReset -and $formattedRuns.Count -gt 0) { try { [void](Invoke-AzLocalSideloadedAutoReset -FormattedRuns $formattedRuns -ApiVersion $ApiVersion) } catch { Write-Log -Message "Sideloaded auto-reset failed: $($_.Exception.Message)" -Level Warning } } if ($PassThru) { return $formattedRuns } return } # Multi-cluster mode Write-Log -Message "" -Level Info Write-Log -Message "========================================" -Level Header Write-Log -Message "Azure Local Update Runs (Fleet)" -Level Header Write-Log -Message "========================================" -Level Header # Verify Azure CLI is installed and logged in Test-AzCliAvailable | Out-Null try { $null = az account show 2>$null if ($LASTEXITCODE -ne 0) { throw "Azure CLI is not logged in. Please run 'az login' first." } Write-Log -Message "Azure CLI authentication verified" -Level Success } catch { Write-Log -Message "Azure CLI is not logged in. Please run 'az login' first." -Level Error return } # Build list of clusters to process $clustersToProcess = @() if ($PSCmdlet.ParameterSetName -eq 'ByTag') { if (-not (Install-AzGraphExtension)) { Write-Error "Failed to install Azure CLI 'resource-graph' extension." return } Write-Log -Message "Querying Azure Resource Graph for clusters with tag 'UpdateRing' = '$UpdateRingValue'..." -Level Info $argQuery = "resources | where type =~ 'microsoft.azurestackhci/clusters' | where tags['UpdateRing'] =~ '$($UpdateRingValue -replace "'", "''")' | project id, name, resourceGroup, subscriptionId, tags" try { $clusterRows = Invoke-AzResourceGraphQuery -Query $argQuery if (-not $clusterRows -or $clusterRows.Count -eq 0) { Write-Log -Message "No clusters found with tag 'UpdateRing' = '$UpdateRingValue'" -Level Warning return @() } Write-Log -Message "Found $($clusterRows.Count) cluster(s) matching tag criteria" -Level Success foreach ($cluster in $clusterRows) { $clustersToProcess += @{ ResourceId = $cluster.id Name = $cluster.name ResourceGroup = $cluster.resourceGroup SubscriptionId = $cluster.subscriptionId } } } catch { Write-Log -Message "Error querying Azure Resource Graph: $_" -Level Error return } } elseif ($PSCmdlet.ParameterSetName -eq 'ByResourceId') { foreach ($resourceId in $ClusterResourceIds) { $clusterRgName = ($resourceId -split '/resourceGroups/')[1] -split '/' | Select-Object -First 1 $clusterSubId = ($resourceId -split '/subscriptions/')[1] -split '/' | Select-Object -First 1 $clustersToProcess += @{ ResourceId = $resourceId Name = ($resourceId -split '/')[-1] ResourceGroup = $clusterRgName SubscriptionId = $clusterSubId } } } else { # ByName - resolve names to resource IDs upfront to avoid per-cluster lookups if (-not $SubscriptionId) { $SubscriptionId = (az account show --query id -o tsv) } foreach ($name in $ClusterNames) { $clusterInfo = Get-AzureLocalClusterInfo -ClusterName $name ` -ResourceGroupName $ResourceGroupName -SubscriptionId $SubscriptionId -ApiVersion $ApiVersion if ($clusterInfo) { $clustersToProcess += @{ ResourceId = $clusterInfo.id Name = $clusterInfo.name ResourceGroup = ($clusterInfo.id -split '/resourceGroups/')[1] -split '/' | Select-Object -First 1 SubscriptionId = ($clusterInfo.id -split '/subscriptions/')[1] -split '/' | Select-Object -First 1 } } else { Write-Log -Message "Cluster '$name' not found - skipping" -Level Warning } } } Write-Log -Message "" -Level Info Write-Log -Message "Querying update runs for $($clustersToProcess.Count) cluster(s)..." -Level Info # Collect results $allFormattedRuns = [System.Collections.Generic.List[object]]::new() $stateCounts = @{} # Per-cluster update-runs scriptblock. Runs inline (ThrottleLimit=1) # or inside Start-Job (ThrottleLimit>1). Emits a structured shape the # parent replays deterministically: Rows (formatted run rows already # flattened) plus LatestState for tally + coloured display. Format- # AzLocalUpdateRun and Get-AzLocalClusterUpdateRuns are module-private # (filtered out by Export-ModuleMember), so when this scriptblock runs # inside a Start-Job runspace they are NOT visible at script scope after # Import-Module. We therefore re-import the module with -PassThru and # invoke the private helpers via the module's own session state using # & $module { ... }, which is the supported pattern for reaching # non-exported helpers from a child runspace. The inline (ThrottleLimit=1) # path runs in the parent runspace where the module's script scope is # already active, so the same scriptblock works there too because # Get-Module returns the already-loaded module. $runsJob = { param( [object[]]$Shard, [string]$ApiVer, [string]$UpdateNameFilter, [bool]$LatestOnly, [string]$ModulePath ) # Always resolve a module reference (PassThru import in child runspace, # already-loaded module in the inline parent runspace). $mod is then # used to bridge into the module's session state for private helpers. $mod = Get-Module -Name AzLocal.UpdateManagement | Select-Object -First 1 if (-not $mod) { $mod = Import-Module $ModulePath -Force -PassThru -ErrorAction Stop } $out = foreach ($cluster in $Shard) { $clusterName = $cluster.Name try { $resourceId = $cluster.ResourceId if (-not $resourceId) { $clusterInfo = Get-AzureLocalClusterInfo -ClusterName $clusterName ` -ResourceGroupName $cluster.ResourceGroup ` -SubscriptionId $cluster.SubscriptionId ` -ApiVersion $ApiVer if ($clusterInfo) { $resourceId = $clusterInfo.id } } if (-not $resourceId) { [PSCustomObject]@{ ClusterName = $clusterName DisplayTag = 'NotFound' LatestState = $null RunCount = 0 Rows = @([PSCustomObject]@{ ClusterName = $clusterName ClusterResourceId = $null UpdateName = 'N/A' RunId = '' State = 'Cluster Not Found' StartTime = '' EndTime = '' Duration = '' Progress = '' CurrentStep = '' CurrentStepDetail = '' Location = '' }) } continue } $runs = @(& $mod { param($rid, $filter, $ver) Get-AzLocalClusterUpdateRuns -resourceId $rid -updateNameFilter $filter -apiVer $ver } $resourceId $UpdateNameFilter $ApiVer) if ($runs.Count -gt 0) { $latestRun = $runs | Sort-Object { $_.properties.timeStarted } -Descending | Select-Object -First 1 $latestState = $latestRun.properties.state $runsToFormat = if ($LatestOnly) { @($latestRun) } else { $runs } $rows = foreach ($run in $runsToFormat) { $formatted = & $mod { param($r, $cn, $crid) Format-AzLocalUpdateRun -run $r -clusterName $cn -clusterResourceId $crid } $run $clusterName $resourceId [PSCustomObject]@{ ClusterName = $clusterName ClusterResourceId = $resourceId UpdateName = $formatted.UpdateName RunId = $formatted.RunId State = $formatted.State StartTime = $formatted.StartTime EndTime = $formatted.EndTime Duration = $formatted.Duration Progress = $formatted.Progress CurrentStep = $formatted.CurrentStep CurrentStepDetail = $formatted.CurrentStepDetail Location = $formatted.Location } } [PSCustomObject]@{ ClusterName = $clusterName DisplayTag = 'Runs' LatestState = $latestState RunCount = $runs.Count Rows = @($rows) } } else { [PSCustomObject]@{ ClusterName = $clusterName DisplayTag = 'NoRuns' LatestState = $null RunCount = 0 Rows = @([PSCustomObject]@{ ClusterName = $clusterName ClusterResourceId = $resourceId UpdateName = 'None' RunId = '' State = 'No Runs' StartTime = '' EndTime = '' Duration = '' Progress = '' CurrentStep = '' CurrentStepDetail = '' Location = '' }) } } } catch { $msg = $_.Exception.Message [PSCustomObject]@{ ClusterName = $clusterName DisplayTag = "Error:$msg" LatestState = $null RunCount = 0 Rows = @([PSCustomObject]@{ ClusterName = $clusterName ClusterResourceId = $resourceId UpdateName = 'Error' RunId = '' State = 'Error' StartTime = '' EndTime = '' Duration = '' Progress = '' CurrentStep = $msg CurrentStepDetail = $msg Location = '' }) } } } return , @($out) } $shardInputs = @($clustersToProcess | ForEach-Object { [PSCustomObject]@{ ResourceId = $_.ResourceId Name = $_.Name ResourceGroup = $_.ResourceGroup SubscriptionId = $_.SubscriptionId } }) $latestOnly = [bool]$Latest $jobResults = Invoke-FleetJobsInParallel ` -InputItems $shardInputs ` -ScriptBlock $runsJob ` -ThrottleLimit $ThrottleLimit ` -ArgumentList @($ApiVersion, [string]$UpdateName, $latestOnly) ` -ActivityName 'UpdateRuns' # Merge shard outputs into a hash keyed by ClusterName for ordered replay. $perCluster = @{} foreach ($jr in $jobResults) { if ($jr.Failed) { foreach ($item in @($jr.Items)) { $perCluster[$item.Name] = [PSCustomObject]@{ ClusterName = $item.Name DisplayTag = "Error:Batch job failed: $($jr.Error)" LatestState = $null RunCount = 0 Rows = @([PSCustomObject]@{ ClusterName = $item.Name ClusterResourceId = $item.ResourceId UpdateName = 'Error' RunId = '' State = 'Error' StartTime = '' EndTime = '' Duration = '' Progress = '' CurrentStep = "Batch job failed: $($jr.Error)" CurrentStepDetail = "Batch job failed: $($jr.Error)" Location = '' }) } } continue } foreach ($entry in @($jr.Output)) { if (-not $entry -or -not $entry.ClusterName) { continue } $perCluster[$entry.ClusterName] = $entry } } foreach ($cluster in $clustersToProcess) { $entry = $perCluster[$cluster.Name] if (-not $entry) { continue } Write-Host " Checking: $($cluster.Name)..." -ForegroundColor Gray -NoNewline switch -Regex ($entry.DisplayTag) { '^NotFound$' { Write-Host ' Not Found' -ForegroundColor Red } '^NoRuns$' { Write-Host ' No runs' -ForegroundColor Gray } '^Error:(.*)' { Write-Host " Error: $($matches[1])" -ForegroundColor Red } '^Runs$' { $stateColor = switch ($entry.LatestState) { 'Succeeded' { 'Green' } 'InProgress' { 'Yellow' } 'Failed' { 'Red' } default { 'Gray' } } Write-Host " $($entry.RunCount) run(s), latest: $($entry.LatestState)" -ForegroundColor $stateColor if ($entry.LatestState) { if ($stateCounts.ContainsKey($entry.LatestState)) { $stateCounts[$entry.LatestState]++ } else { $stateCounts[$entry.LatestState] = 1 } } } default { Write-Host '' -ForegroundColor Gray } } foreach ($row in @($entry.Rows)) { $allFormattedRuns.Add($row) | Out-Null } } # Display Summary Write-Log -Message "" -Level Info Write-Log -Message "========================================" -Level Header Write-Log -Message "Summary" -Level Header Write-Log -Message "========================================" -Level Header $totalClusters = $clustersToProcess.Count Write-Log -Message "" -Level Info Write-Log -Message "Total Clusters: $totalClusters" -Level Info if ($stateCounts.Count -gt 0) { Write-Log -Message "Latest Run States:" -Level Header foreach ($state in $stateCounts.Keys | Sort-Object) { $level = switch ($state) { "Succeeded" { "Success" } "Failed" { "Error" } "InProgress" { "Warning" } default { "Info" } } Write-Log -Message " $state`: $($stateCounts[$state])" -Level $level } } # Display results table Write-Log -Message "" -Level Info Write-Log -Message "Update Runs:" -Level Header $allFormattedRuns | Format-Table ClusterName, UpdateName, State, StartTime, EndTime, Duration, Progress -AutoSize | Out-Host # Check for health-check-blocked failures and show diagnostics $healthBlockedRuns = @($allFormattedRuns | Where-Object { $_.State -eq "Failed" -and $_.CurrentStep -match "health check" }) if ($healthBlockedRuns.Count -gt 0) { $affectedClusters = @($healthBlockedRuns | Select-Object -ExpandProperty ClusterName -Unique) Write-Log -Message "" -Level Info Write-Log -Message "Detected $($healthBlockedRuns.Count) update run(s) blocked by health check failures." -Level Warning Write-Log -Message "Querying current health check status for affected cluster(s)..." -Level Info foreach ($affectedCluster in $affectedClusters) { # Find the resource ID for this cluster from the clusters we already processed $clusterEntry = $clustersToProcess | Where-Object { $_.Name -eq $affectedCluster } $rid = $clusterEntry.ResourceId if (-not $rid) { continue } $healthResults = Test-AzureLocalClusterHealth -ClusterResourceIds @($rid) -BlockingOnly if ($healthResults -and $healthResults[0].CriticalCount -gt 0) { Write-Log -Message "" -Level Info Write-Log -Message "Critical health issues blocking updates on '$affectedCluster':" -Level Error foreach ($failure in $healthResults[0].Failures) { $nodeInfo = if ($failure.TargetResourceName) { " (Node: $($failure.TargetResourceName))" } else { "" } Write-Log -Message " [Critical] $($failure.CheckName)$nodeInfo`: $($failure.Description)" -Level Error if ($failure.Remediation) { Write-Log -Message " Remediation: $($failure.Remediation)" -Level Warning } } } } } # Export if path specified if ($ExportPath) { try { $ExportPath = Resolve-SafeOutputPath -Path $ExportPath $exportDir = Split-Path -Path $ExportPath -Parent if ($exportDir -and -not (Test-Path $exportDir)) { New-Item -ItemType Directory -Path $exportDir -Force | Out-Null } $format = Get-ExportFormat -Path $ExportPath -ExportFormat $ExportFormat switch ($format) { 'Csv' { $allFormattedRuns | ConvertTo-SafeCsvCollection | Export-Csv -Path $ExportPath -NoTypeInformation -Encoding UTF8 Write-Log -Message "Results exported to CSV: $ExportPath" -Level Success } 'Json' { $exportData = @{ Timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" TotalClusters = $totalClusters StateSummary = $stateCounts Results = $allFormattedRuns } Write-Utf8NoBomFile -Path $ExportPath -Content ($exportData | ConvertTo-Json -Depth 10) Write-Log -Message "Results exported to JSON: $ExportPath" -Level Success } 'JUnitXml' { $junitResults = $allFormattedRuns | ForEach-Object { [PSCustomObject]@{ ClusterName = $_.ClusterName Status = if ($_.State -eq "Succeeded") { "Passed" } elseif ($_.State -in @("Failed", "Error")) { "Failed" } else { "Skipped" } Message = "Update: $($_.UpdateName), State: $($_.State), Duration: $($_.Duration), Progress: $($_.Progress)" UpdateName = $_.UpdateName CurrentState = $_.State StartTime = $_.StartTime EndTime = $_.EndTime Duration = $_.Duration Progress = $_.Progress } } Export-ResultsToJUnitXml -Results $junitResults -OutputPath $ExportPath ` -TestSuiteName "AzureLocalUpdateRuns" -OperationType "UpdateRuns" Write-Log -Message "Results exported to JUnit XML: $ExportPath" -Level Success } } } catch { Write-Log -Message "Failed to export results: $($_.Exception.Message)" -Level Error } } Write-Log -Message "" -Level Info # Display latest run details per cluster if ($allFormattedRuns.Count -gt 0) { $latestPerCluster = $allFormattedRuns | Group-Object ClusterName | ForEach-Object { $_.Group | Sort-Object StartTime -Descending | Select-Object -First 1 } Write-Log -Message "Latest Update Run per Cluster:" -Level Header Write-Host "" $latestPerCluster | Format-List | Out-String -Stream | ForEach-Object { if ($_ -ne "") { Write-Host "`t$_" } } Write-Host "" } # v0.7.1: Sideloaded auto-reset (default ON; -SkipSideloadedReset to disable). if (-not $SkipSideloadedReset -and $allFormattedRuns.Count -gt 0) { try { [void](Invoke-AzLocalSideloadedAutoReset -FormattedRuns $allFormattedRuns -ApiVersion $ApiVersion) } catch { Write-Log -Message "Sideloaded auto-reset failed: $($_.Exception.Message)" -Level Warning } } if ($PassThru) { return $allFormattedRuns } } |