AzLocal.UpdateManagement
0.8.6
PowerShell module to manage Azure Local (formerly Azure Stack HCI) cluster updates using Azure Update Manager APIs. Provides functions to start updates, check update status, list available updates, and monitor update runs. Renamed from AzStackHci.ManageUpdates in v0.7.3 to align with the Azure Local product name.
Minimum PowerShell version
5.1
Installation Options
Owners
Copyright
(c) Microsoft. All rights reserved.
Package Details
Author(s)
- Neil Bird Microsoft
Tags
Azure AzureLocal AzureStackHCI Updates UpdateManager HCI Automation CICD Pipeline ServiceNow ITSM Incident
Functions
Connect-AzLocalServicePrincipal Start-AzLocalClusterUpdate Get-AzLocalClusterUpdateReadiness Get-AzLocalClusterInventory Get-AzLocalClusterInfo Get-AzLocalUpdateSummary Get-AzLocalAvailableUpdates Get-AzLocalUpdateRuns Set-AzLocalClusterUpdateRingTag Invoke-AzLocalFleetOperation Get-AzLocalFleetProgress Test-AzLocalFleetHealthGate Export-AzLocalFleetState Resume-AzLocalFleetUpdate Stop-AzLocalFleetUpdate Test-AzLocalClusterHealth Get-AzLocalFleetStatusData New-AzLocalFleetStatusHtmlReport Test-AzLocalUpdateScheduleAllowed Reset-AzLocalSideloadedTag Get-AzLocalItsmConfig Test-AzLocalItsmConnection New-AzLocalIncident Copy-AzLocalPipelineExample Update-AzLocalPipelineExample Copy-AzLocalItsmSample Get-AzLocalFleetHealthFailures Test-AzLocalApplyUpdatesScheduleCoverage Get-AzLocalUpdateRunFailures Get-AzLocalApplyUpdatesScheduleConfig Resolve-AzLocalCurrentUpdateRing Get-AzLocalApplyUpdatesScheduleNextFirings New-AzLocalApplyUpdatesScheduleConfig Update-AzLocalApplyUpdatesScheduleConfig Get-AzLocalApplyUpdatesScheduleCycleCalendar Get-AzLocalFleetHealthOverview Get-AzLocalLatestSolutionVersion Get-AzLocalFleetConnectivityStatus New-AzLocalFleetConnectivityStatusSummary Add-AzLocalPipelineVersionBanner Export-AzLocalAuthValidationReport Invoke-AzLocalClusterInventory Set-AzLocalClusterUpdateRingTagFromCsv Export-AzLocalUpdateRunMonitorReport Export-AzLocalFleetUpdateStatusReport Export-AzLocalClusterUpdateReadinessReport Export-AzLocalFleetConnectivityStatusReport Export-AzLocalApplyUpdatesScheduleAudit Export-AzLocalFleetHealthStatusReport Resolve-AzLocalPipelineUpdateRing Export-AzLocalClusterReadinessGateReport Invoke-AzLocalReadinessGatedClusterUpdate Add-AzLocalApplyUpdatesStepSummary Add-AzLocalNoReadyClustersStepSummary Invoke-AzLocalItsmTicketingFromArtifact
PSEditions
Dependencies
This module has no dependencies.
Release Notes
## Version 0.8.6 - Step.3 cycle-calendar enrichment (per-day CRON + UpdateStartWindow tag coverage) + six v0.8.5 pipeline regression fixes (Step.0/3/4/6/9) + new Pester static-audit guards
Adds two opt-in render-time columns to `Get-AzLocalApplyUpdatesScheduleCycleCalendar` (auto-wired by `Export-AzLocalApplyUpdatesScheduleAudit`) AND fixes six production regressions introduced by the v0.8.5 thin-YAML port. New static-source Pester guards block the underlying anti-patterns from shipping again. No public API removed; no parameter changes on existing cmdlets; same module export count as v0.8.5 (55).
- **FIX Step.0 (`Export-AzLocalAuthValidationReport`)**: `Resource Graph reachability` reported "1 cluster(s) visible" no matter the real fleet size. `Invoke-AzResourceGraphQuery` uses unary-comma `return , $arr.ToArray()`; wrapping it with `@()` collected the outer wrapper and dropped every row past the first. Switched to direct assignment + `$null` -> `@()` guard, plus a defensive `$clusterRows = @($clusterRows)` coerce on the variable so `.Count` is always a valid integer property under strict mode.
- **FIX Step.3 (`Test-AzLocalApplyUpdatesScheduleCoverage`)**: `$allMatched = @($segmentStatuses.MatchingCrons | Select-Object -Unique)` threw "The property 'MatchingCrons' cannot be found on this object." for any audit row where `$r.RequiredCrons` was empty - `$segmentStatuses` stayed `@()` and member-enumeration on an empty array trips strict mode. Guarded the access with `if ($segmentStatuses.Count -gt 0)`.
- **FIX Step.4 (`Get-AzLocalFleetConnectivityStatus`)**: ARB row rendering crashed under `Set-StrictMode -Version Latest` on any RG holding exactly one cluster. The if-as-expression silently unwrapped the single-element `@()` to a scalar PSCustomObject; `$matched.Count` then threw. The v0.8.6-fix1 `[object[]]$matched = if (...)` cast was insufficient on the production fleet (still scalar at runtime). The reliable fix: assign the raw list (or `$null`) to `$matchedList`, then `$matched = @($matchedList)` to coerce scalar -> Object[1] on the *variable* (idempotent for Object[N]).
- **FIX Step.6 (`Get-AzLocalClusterUpdateReadiness`)**: `if ($cluster.NotFound)` at line ~337 threw "The property 'NotFound' cannot be found on this object" because three of the four `$clustersToProcess += @{ ... }` builder paths omitted the key. Added `NotFound = $false` to all three missing-key paths so the strict-mode iteration is always safe.
- **FIX Step.9 (`Export-AzLocalFleetHealthStatusReport`)**: BOTH `$detail = @(Get-AzLocalFleetHealthFailures ...)` and `$overview = @(Get-AzLocalFleetHealthOverview ...)` collapsed the row-set to `Object[1]`; "Found 81 failing check(s)" from the helper became "Found 1" two lines later and Group-Object then crashed with exit code 1. Switched both to direct assignment + `$null` -> `@()` guards.
- **FIX Node.js 20 deprecation**: bumped `actions/upload-artifact@v4` -> `@v6` in `Step.0_authentication-test.yml` (every other Step YAML was already on `@v6`).
- **FIX Step.2 per-cluster `Message` now names the tags that actually changed.** When only a schedule tag (e.g. `UpdateExcluded`) differs between cluster and CSV, the summary used to read "UpdateRing tag updated successfully" - misleading because `UpdateRing` was unchanged. `Set-AzLocalClusterUpdateRingTag` now computes per-tag deltas from `$tagsToMerge.Keys` vs `$currentTags` and writes `Tags updated: UpdateExcluded: 'False' -> 'True'` (same shape for `-WhatIf`).
- **FIX Step.2 summary table split into outcome buckets.** `Set-AzLocalClusterUpdateRingTagFromCsv` previously rendered all clusters (created/updated/no-op/skipped/failed) into one `<details>` block. The summary now renders three independent collapsible sections, each shown only when non-empty: **"Clusters with Tag Updates Applied (N rows)"** (expanded), **"Clusters Skipped or Failed (N rows)"** (expanded), **"Clusters with No Tag Updates (no-op) (N rows)"** (collapsed).
- **FIX Step.2 `Total clusters processed` count was inflated (`44` vs `20` on a 20-row CSV).** `Set-AzLocalClusterUpdateRingTag` ended with `$results | Format-Table -AutoSize` (no `| Out-Host`), so the formatter wrapper objects leaked into the function's pipeline output and mixed with `return $results` under `-PassThru`. Fixed by piping Format-Table to `Out-Host` + defence-in-depth filter in `Set-AzLocalClusterUpdateRingTagFromCsv` that drops any object without a string `ClusterName` property.
- **NEW Pester regression guards** (11 It blocks across 7 Describe blocks): static-source scans for `@()` wrap on unary-comma helpers (`Invoke-AzResourceGraphQuery`, `Get-AzLocalFleetHealth{Failures,Overview}`, `Read-AzLocalApplyUpdatesYamlCrons`); Step.3 `.MatchingCrons` strict-mode crash; Step.4 `$matched if/else { @() }` branch; Step.6 `NotFound` key on every `$clustersToProcess` builder; Step.0 `$clusterRows` variable-level `@()` coerce; Step.2 `Format-Table | Out-Host`; GHA Node.js 20 artifact-action version deprecation; plus functional spy tests for Export-AzLocalAuthValidationReport + Export-AzLocalFleetHealthStatusReport.
- **NEW `-CronFiringsByDate`** (`[hashtable]`, keys = `yyyy-MM-dd` UTC, values = `[string[]]` of `HH:mm` UTC firing times). Adds centered `Ring CRON Start Time (UTC)<br>(Step 6 pipeline)` column between `Date (UTC)` and `Day`. Cell rendering: 0 firings -> `_(none)_`; 1-2 -> comma-joined; 3+ -> first 2 + `" (+N)"`; dead day -> `_(none - dead day)_`; missing date key -> blank.
- **NEW `-WindowMatchByRingAndDate`** (`[hashtable[string,hashtable]]`, ring -> date -> `@{ Matching=<int>; Total=<int> }`). Adds `Tag Start Window Match (>=95%)` column AFTER `Eligible rings`. Per-ring line: `` `Ring`: True/False mat/tot (pct%) `` (True iff `Matching/Total >= 0.95`). Rendering: `_(n/a)_` no entry; `_(0 clusters)_` Total=0; `_(n/a - dead day)_` on dead days.
- **Pure render-time contract preserved.** Cmdlet still does zero Azure / file I/O. Both columns opt-in.
- **`Export-AzLocalApplyUpdatesScheduleAudit` auto-wires both.** Cron firings via existing `Read-AzLocalApplyUpdatesYamlCrons` + `ConvertFrom-AzLocalCronExpression` (invalid crons -> no firings for that day). Window-match via `ConvertFrom-AzLocalUpdateWindow` per cluster (when `-ClusterCsvPath` supplied); overnight windows match firings in either the late-evening or early-morning portion via two-case `DayOfWeek` projection.
- **Failure mode is non-fatal.** Enrichment errors degrade to the v0.8.5 calendar; `Write-Warning` surfaces the cause but Step.3 summary continues to render.
- **Pester**: drift bumped to `'0.8.6'`; 12 new It blocks cover backwards compat, single-param paths, both-params 7-column header, `(+N)` suffix at 3+ firings, 95% threshold boundary, `_(0 clusters)_` / `_(n/a)_` rendering, ring + date case-insensitivity, and coexistence with `-ClusterRingCounts`. 2 new Export-* spy tests verify the auto-wire path. PLUS 11 new regression-guard It blocks across 7 Describe blocks.
- **All 20 bundled `Step.{0..9}.yml` templates** bump `GENERATED_AGAINST_MODULE_VERSION` from `'0.8.5'` to `'0.8.6'`.
## Version 0.8.5 - New Public cmdlet `Get-AzLocalApplyUpdatesScheduleCycleCalendar` + Step.6 manual schedule-file inputs + Step.3 cycle-calendar regression fix + per-ring cluster-count column + full thin-YAML port of all 10 Step pipelines (14 new Public cmdlets). Module export count grows 35 -> 55. See CHANGELOG for the full v0.8.5 entry.
## Version 0.8.3 - Test-AzLocalApplyUpdatesScheduleCoverage Step.3 advisor accuracy + readability fixes: Recommend now diff-prunes against `-PipelineYamlPath`, Step.3 yml `pipeline_path` REQUIRED, Allow-list heading reframed, closing-fence typo fixed
## Version 0.8.2 - Test-AzLocalApplyUpdatesScheduleCoverage operator-UX release: -View Recommend snippet embeds `# All cron times below are UTC` comment + `Indent tip` blockquote; -View Audit `NoWindowTag` row now names affected clusters grouped by `UpdateRing` + sorts AFTER Covered; Step.3 GH/ADO Allow-list section trimmed; five new internal pipeline-host helpers (Get/Set/Add/Write-AzLocalPipeline*) laid down as foundations for the upcoming executable-YAML refactor
## Version 0.8.1 - Test-AzLocalApplyUpdatesScheduleCoverage -View Recommend GH snippet emits ONLY the `schedule:` block (no `on:` / `workflow_dispatch:` lines) so it can be pasted straight into Step.6_apply-updates.yml without producing a duplicate-key YAML error
## Version 0.8.0 - Step.7 form-default regressions fixed (criticalElapsedDays 7->3, updateRing Wave1->empty) + Pii-Guard.Tests.ps1 (repo-hygiene guard) + Publish-Module.ps1 excludes maintainer-only RELEASE-PROCESS.md
## Version 0.7.99 - Property/Summary renames (AvailableUpdates -> AllAvailableUpdates, AvailableUpdatesCount -> ActionableUpdatesCount, Ready/NotReady Summary -> ReadyForUpdate/UpToDate/NotReadyForUpdate) + Step.7 CRITICAL elapsed-days 7->3 + artifact zip names prefixed with step.X-
For full v0.7.x and v0.8.x release notes see:
https://github.com/NeilBird/Azure-Local/blob/main/AzLocal.UpdateManagement/CHANGELOG.md
FileList
- AzLocal.UpdateManagement.nuspec
- Automation-Pipeline-Examples\azure-devops\Step.3_apply-updates-schedule-audit.yml
- Automation-Pipeline-Examples\github-actions\Step.5_assess-update-readiness.yml
- ITSM\ITSM-Config-Reference.md
- Private\ConvertTo-SafeCsvCollection.ps1
- Private\Get-CurrentStepPath.ps1
- Private\Invoke-AzLocalSideloadedAutoResetForCluster.ps1
- Private\Set-AzLocalPipelineOutput.ps1
- Public\Add-AzLocalApplyUpdatesStepSummary.ps1
- Public\Export-AzLocalUpdateRunMonitorReport.ps1
- Public\Get-AzLocalLatestSolutionVersion.ps1
- Public\Resolve-AzLocalPipelineUpdateRing.ps1
- AzLocal.UpdateManagement.psd1
- Automation-Pipeline-Examples\azure-devops\Step.4_fleet-connectivity-status.yml
- Automation-Pipeline-Examples\github-actions\Step.6_apply-updates.yml
- ITSM\ITSM-Connector-Plan.md
- Private\ConvertTo-SafeCsvField.ps1
- Private\Get-DeepestActiveStep.ps1
- Private\Invoke-AzLocalUpdateApply.ps1
- Private\Test-AzCliAvailable.ps1
- Public\Add-AzLocalNoReadyClustersStepSummary.ps1
- Public\Get-AzLocalApplyUpdatesScheduleConfig.ps1
- Public\Get-AzLocalUpdateRunFailures.ps1
- Public\Resume-AzLocalFleetUpdate.ps1
- AzLocal.UpdateManagement.psm1
- Automation-Pipeline-Examples\azure-devops\Step.5_assess-update-readiness.yml
- Automation-Pipeline-Examples\github-actions\Step.7_monitor-updates.yml
- ITSM\README.md
- Private\ConvertTo-ScrubbedCliOutput.ps1
- Private\Get-DeepestErrorMessage.ps1
- Private\Invoke-AzResourceGraphQuery.ps1
- Private\Test-AzLocalAllowedUpdateVersionsString.ps1
- Public\Add-AzLocalPipelineVersionBanner.ps1
- Public\Get-AzLocalApplyUpdatesScheduleCycleCalendar.ps1
- Public\Get-AzLocalUpdateRuns.ps1
- Public\Set-AzLocalClusterUpdateRingTag.ps1
- example-update-request.json
- Automation-Pipeline-Examples\azure-devops\Step.6_apply-updates.yml
- Automation-Pipeline-Examples\github-actions\Step.8_fleet-update-status.yml
- Private\Add-AzLocalPipelineStepSummary.ps1
- Private\Export-ResultsToJUnitXml.ps1
- Private\Get-ExportFormat.ps1
- Private\Invoke-AzRestJson.ps1
- Private\Test-AzLocalUpdateExcludedAllowed.ps1
- Public\Connect-AzLocalServicePrincipal.ps1
- Public\Get-AzLocalApplyUpdatesScheduleNextFirings.ps1
- Public\Get-AzLocalUpdateSummary.ps1
- Public\Set-AzLocalClusterUpdateRingTagFromCsv.ps1
- README.md
- Automation-Pipeline-Examples\azure-devops\Step.7_monitor-updates.yml
- Automation-Pipeline-Examples\github-actions\Step.9_fleet-health-status.yml
- Private\Convert-AzLocalScheduleSchemaVersion.ps1
- Private\Format-AzLocalDurationHuman.ps1
- Private\Get-HealthCheckFailureSummary.ps1
- Private\Invoke-FleetJobsInParallel.ps1
- Private\Test-AzLocalUpdateExclusion.ps1
- Public\Copy-AzLocalItsmSample.ps1
- Public\Get-AzLocalAvailableUpdates.ps1
- Public\Invoke-AzLocalClusterInventory.ps1
- Public\Start-AzLocalClusterUpdate.ps1
- Test-Pipelines.ps1
- Automation-Pipeline-Examples\azure-devops\Step.8_fleet-update-status.yml
- docs\cmdlet-reference.md
- Private\Convert-AzLocalUpdateWindowToCron.ps1
- Private\Format-AzLocalIncidentBody.ps1
- Private\Get-LastUpdateRunErrorSummary.ps1
- Private\Invoke-FleetOpClusterAction.ps1
- Private\Test-AzLocalUpdateSideloadedAllowed.ps1
- Public\Copy-AzLocalPipelineExample.ps1
- Public\Get-AzLocalClusterInfo.ps1
- Public\Invoke-AzLocalFleetOperation.ps1
- Public\Stop-AzLocalFleetUpdate.ps1
- Automation-Pipeline-Examples\apply-updates-schedule.example.yml
- Automation-Pipeline-Examples\azure-devops\Step.9_fleet-health-status.yml
- docs\concepts.md
- Private\ConvertFrom-AzLocalCronExpression.ps1
- Private\Format-AzLocalUpdateRun.ps1
- Private\Get-LatestUpdateByYYMM.ps1
- Private\New-AzLocalPipelineJUnitXml.ps1
- Private\Test-AzLocalUpdateVersionInProgressMatch.ps1
- Public\Export-AzLocalApplyUpdatesScheduleAudit.ps1
- Public\Get-AzLocalClusterInventory.ps1
- Public\Invoke-AzLocalItsmTicketingFromArtifact.ps1
- Public\Test-AzLocalApplyUpdatesScheduleCoverage.ps1
- Automation-Pipeline-Examples\azlocal-update-management-custom-role.json
- Automation-Pipeline-Examples\docs\appendix-pipelines.md
- docs\rbac.md
- Private\ConvertFrom-AzLocalScheduleYaml.ps1
- Private\Get-AzLocalClusterUpdateRuns.ps1
- Private\Get-TagValue.ps1
- Private\Read-AzLocalApplyUpdatesYamlCrons.ps1
- Private\Test-AzLocalUpdateWindow.ps1
- Public\Export-AzLocalAuthValidationReport.ps1
- Public\Get-AzLocalClusterUpdateReadiness.ps1
- Public\Invoke-AzLocalReadinessGatedClusterUpdate.ps1
- Public\Test-AzLocalClusterHealth.ps1
- Automation-Pipeline-Examples\README.md
- Automation-Pipeline-Examples\docs\appendix-release-history.md
- docs\release-history.md
- Private\ConvertFrom-AzLocalUpdateExcluded.ps1
- Private\Get-AzLocalItsmDedupeKey.ps1
- Private\Import-AzLocalFleetState.ps1
- Private\Resolve-AzLocalItsmSecret.ps1
- Private\Test-ExportPathWritable.ps1
- Public\Export-AzLocalClusterReadinessGateReport.ps1
- Public\Get-AzLocalFleetConnectivityStatus.ps1
- Public\New-AzLocalApplyUpdatesScheduleConfig.ps1
- Public\Test-AzLocalFleetHealthGate.ps1
- Automation-Pipeline-Examples\.itsm\azurelocal-itsm.yml
- Automation-Pipeline-Examples\github-actions\Step.0_authentication-test.yml
- docs\troubleshooting.md
- Private\ConvertFrom-AzLocalUpdateExclusion.ps1
- Private\Get-AzLocalItsmTriggerDecision.ps1
- Private\Install-AzGraphExtension.ps1
- Private\Resolve-AzLocalUpdateRunDeepestError.ps1
- Private\Write-AzLocalPipelineNotice.ps1
- Public\Export-AzLocalClusterUpdateReadinessReport.ps1
- Public\Get-AzLocalFleetHealthFailures.ps1
- Public\New-AzLocalFleetConnectivityStatusSummary.ps1
- Public\Test-AzLocalItsmConnection.ps1
- Automation-Pipeline-Examples\.itsm\templates\incident-body.md
- Automation-Pipeline-Examples\github-actions\Step.1_inventory-clusters.yml
- docs\images\apply-updates-summary.png
- Private\ConvertFrom-AzLocalUpdateSideloaded.ps1
- Private\Get-AzLocalModuleRootManifestPath.ps1
- Private\Invoke-AzCliJson.ps1
- Private\Resolve-SafeOutputPath.ps1
- Private\Write-AzLocalPipelineWarning.ps1
- Public\Export-AzLocalFleetConnectivityStatusReport.ps1
- Public\Get-AzLocalFleetHealthOverview.ps1
- Public\New-AzLocalFleetStatusHtmlReport.ps1
- Public\Test-AzLocalUpdateScheduleAllowed.ps1
- Automation-Pipeline-Examples\azure-devops\Step.0_authentication-test.yml
- Automation-Pipeline-Examples\github-actions\Step.2_manage-updatering-tags.yml
- docs\images\auth-smoke-test-validate-oidc.png
- Private\ConvertFrom-AzLocalUpdateWindow.ps1
- Private\Get-AzLocalPipelineCustomiseMarkers.ps1
- Private\Invoke-AzLocalItsmHttp.ps1
- Private\Resolve-WildcardDate.ps1
- Private\Write-Log.ps1
- Public\Export-AzLocalFleetHealthStatusReport.ps1
- Public\Get-AzLocalFleetProgress.ps1
- Public\New-AzLocalIncident.ps1
- Public\Update-AzLocalApplyUpdatesScheduleConfig.ps1
- Automation-Pipeline-Examples\azure-devops\Step.1_inventory-clusters.yml
- Automation-Pipeline-Examples\github-actions\Step.3_apply-updates-schedule-audit.yml
- docs\images\inventory-clusters-run-output.png
- Private\ConvertTo-AzLocalAdditionalProperties.ps1
- Private\Get-AzLocalPipelineHost.ps1
- Private\Invoke-AzLocalServiceNowAdapter.ps1
- Private\Resolve-WildcardDateRange.ps1
- Private\Write-UpdateCsvLog.ps1
- Public\Export-AzLocalFleetState.ps1
- Public\Get-AzLocalFleetStatusData.ps1
- Public\Reset-AzLocalSideloadedTag.ps1
- Public\Update-AzLocalPipelineExample.ps1
- Automation-Pipeline-Examples\azure-devops\Step.2_manage-updatering-tags.yml
- Automation-Pipeline-Examples\github-actions\Step.4_fleet-connectivity-status.yml
- docs\images\README.md
- Private\ConvertTo-AzLocalUpdateRingKqlFilter.ps1
- Private\Get-AzLocalRunEndTime.ps1
- Private\Invoke-AzLocalSideloadedAutoReset.ps1
- Private\Set-AzLocalClusterTagsMerge.ps1
- Private\Write-Utf8NoBomFile.ps1
- Public\Export-AzLocalFleetUpdateStatusReport.ps1
- Public\Get-AzLocalItsmConfig.ps1
- Public\Resolve-AzLocalCurrentUpdateRing.ps1
Version History
| Version | Downloads | Last updated |
|---|---|---|
| 0.8.6 (current version) | 17 | 6/10/2026 |
| 0.8.5 | 15 | 6/10/2026 |
| 0.8.4 | 27 | 6/9/2026 |
| 0.8.3 | 10 | 6/9/2026 |
| 0.8.2 | 6 | 6/9/2026 |