What you will find here: A chronological record of every released version, in newest-first order. Each entry describes user-visible behaviour changes, bug fixes, and migration steps. The most recent release's full What's-New is also mirrored in the main README.md under
What's New in v<latest>.For older releases, this is the canonical reference; the main README intentionally stays slim so the most recent block is easy to find.
For v0.8.78 (the current release), see the main README.md
What's New in v0.8.78section.
v0.8.77 fixed two production strict-mode crashes that surfaced in Step.05 / Step.06 / Step.07 of the bundled apply-updates pipelines. Both bugs shared the same root cause: bare $obj.Prop property access under Set-StrictMode -Version Latest throws when Prop is absent on a PSCustomObject instead of returning $null.
Start-AzLocalClusterUpdate(Step.07 main entry) - production emittedError processing cluster '<Name>': The property 'UpdateStartWindow' cannot be found on this object.for clusters whose tag bag did not includeUpdateStartWindow/UpdateExclusionsWindow. The guard now branches on$clusterTags -is [System.Collections.IDictionary]and uses.Contains()for hashtables /PSObject.Properties[...]forPSCustomObjecttag bags. The semantic intent ("absent = any time eligible / no window restriction") is preserved.Test-AzLocalClusterHealth(Step.05/06 readiness gate) - production emittedChecking: <Cluster>... Error: The property 'healthCheckResult' cannot be found on this object.for clusters whose ARM update summary genuinely had nohealthCheckResultfield. Thecatchblock then flagged the clusterHealthState=Error / Passed=$false, poisoning Step.5 / Step.7 / Step.9 readiness output for affected clusters. Same guard idiom applied inGet-HealthCheckFailureSummary(Private) andGet-AzLocalFleetStatusData(Public,-IncludeHealthDetails). Such clusters now correctly classify asHealthState="No Data" / Passed=$true.
Two new Pester regression contexts feed the minimal real-world property-less shapes and assert Should -Not -Throw. Both bugs were invisible to parse-time analysis - only runtime against the property-less shape triggers the strict-mode throw.
GENERATED_AGAINST_MODULE_VERSION bumped from 0.8.76 to 0.8.77 across all bundled pipeline templates.
See CHANGELOG.md for the full v0.8.77 entry.
v0.8.76 adds a Microsoft-hosted Windows preflight job (GitHub Actions) / preflight stage (Azure DevOps) in front of the opt-in Step.6 sideload-updates.yml pipeline. Before v0.8.76, triggering Step.6 without first completing the opt-in setup (master gate SIDELOAD_UPDATES not set, or set without registering a self-hosted azlocal-sideload runner) produced Status: Skipped with no logs, no annotation, and no actionable feedback. The new preflight job (runs-on: windows-latest, ~10s, no Azure access) ALWAYS runs and writes a clear panel to the run step summary. Master gate broadened to accept 'true' / 'True' / 'TRUE' / '1'. No public API change or new exports (still 60).
GENERATED_AGAINST_MODULE_VERSION bumped from 0.8.75 to 0.8.76 across all bundled pipeline templates.
See CHANGELOG.md for the full v0.8.76 entry.
v0.8.75 removes the duplicate Cycle calendar table from the Step.3 apply-updates schedule audit step summary on runs with Recommend-view findings (uncovered/partial cron windows, RingMissingFromSchedule, RingOrphanedInSchedule, UnparseableCron, or NoWindowTag when a cluster CSV was supplied). The Recommend snippet's plain 5-column calendar was inlined into the wrapper's action-required block above the wrapper's own enriched 7-column calendar, so operators saw two calendars back-to-back. Clean-fleet runs were unaffected. New additive [switch]$OmitCycleCalendar parameter on Test-AzLocalApplyUpdatesScheduleCoverage (default off, so direct callers see no behaviour change); Export-AzLocalApplyUpdatesScheduleAudit opts in so only the enriched calendar surfaces in the Step.3 summary regardless of findings. The drift-notice banner from Add-AzLocalPipelineVersionBanner now recommends Update-AzLocalPipelineExample (marker-aware merge that preserves AZLOCAL-CUSTOMIZE blocks) with the right -Platform and a -Destination hint per detected pipeline host. Three Step.3 step-summary screenshots added to Automation-Pipeline-Examples/README.md section 8.3. No public API or export-count change (still 60).
GENERATED_AGAINST_MODULE_VERSION bumped from 0.8.74 to 0.8.75 across all bundled pipeline templates.
See CHANGELOG.md for the full v0.8.75 entry.
v0.8.74 makes the "Up to Date" cluster classification consistent across the Step.5 readiness report, the Step.7 readiness gate, and the Step.9 fleet update-status report. It also corrects the Progress column in the Step.7 in-flight monitor and the standalone HTML report's "Recent Update Run History" table, both of which had been reporting a near-constant 1/2 steps for the whole multi-hour run, and drops the dead target="_blank" portal-link attributes that GitHub Actions / Azure DevOps step-summary sanitisers were stripping anyway (replaced with an explicit Ctrl/Cmd/middle-click tip above each affected table). No public API or export-count change (still 60).
Fully-patched clusters were mis-classified as "not ready". A cluster that had successfully installed every required update showed 0 in the Step.5 (Export-AzLocalClusterUpdateReadinessReport) "Up to date" summary count and appeared in its "Not-Ready clusters (review first)" table; Step.7 (Export-AzLocalClusterReadinessGateReport) rendered it with a no-entry icon in the binary Ready? column - both implying failure. The root cause was that the "Up to Date" determination required the AllAvailableUpdates collection to be empty, but a cluster that has installed all updates still lists those (now-Installed) package names there. Step.9 already classified these clusters correctly via its priority cascade.
Progress column always reported the top-level wrapper. The Step.7 in-flight monitor (Export-AzLocalUpdateRunMonitorReport) and the standalone HTML report's Recent Update Run History table (New-AzLocalFleetStatusHtmlReport, fed by Get-AzLocalFleetStatusData) both rendered Progress from the top level of properties.progress.steps. Azure Local only exposes two coarse wrapper steps there - Prepare update (Success) and Start update (InProgress for the whole run) - so the column always read 1/2 steps for ~150-leaf solution updates regardless of real progress. Both paths now walk the full nested step tree via a new private helper (Get-AzLocalUpdateRunStepStats) and report leaf-step completion as M/N steps (P%) (optionally suffixed with , K failed), matching how CurrentStep already walks the tree via Get-DeepestActiveStep.
Portal hyperlinks target="_blank" was being silently stripped. The Cluster / Update hyperlinks rendered in Step.7 (Export-AzLocalUpdateRunMonitorReport) in-flight + failed-runs tables, Step.8 (Export-AzLocalFleetUpdateStatusReport) Update Run History table, and Step.10 (Export-AzLocalFleetHealthStatusReport) Fleet Health Overview / By-Reason / Detailed Results tables always opened in the current tab even though the markdown source set target="_blank". GitHub's GFM sanitiser (and the ADO equivalent) drops the target attribute entirely and forces rel="nofollow" - confirmed in the rendered HTML of a real GitHub Actions job summary, and documented in Stack Overflow: open link in new tab with github markdown using target="_blank". The dead attributes are now removed (so the emitted markdown matches the rendered DOM), and each affected table is preceded by an explicit tip: "Hold Ctrl (or Cmd on macOS) when clicking - or middle-click - Cluster or Update links to open them in a new tab. (GitHub markdown strips target="_blank".)" The standalone HTML report (New-AzLocalFleetStatusHtmlReport) is unaffected; it is written as a raw .html artifact (not a step-summary), so its target="_blank" already works as intended in any browser.
New shared classifier. A new private helper, Get-AzLocalClusterReadinessStatus, is the single source of truth for the readiness priority cascade (UpdateFailed > ActionRequired > HealthFailure > SbeBlocked > InProgress > ReadyForUpdate > UpToDate > NeedsInvestigation). Step.5, Step.7 and Step.9 (Export-AzLocalFleetUpdateStatusReport) now classify clusters identically through this helper; previously each report re-implemented the logic inline and the definitions had drifted.
Step.7 reporting changes. The per-cluster table replaces its binary Ready? column with a readable Status column (Ready, Up to Date, In Progress, SBE Prerequisite, Health Failure, Update Failed, Action Required, Needs Investigation), the header line shows a distinct Up to Date count, and a new additive UP_TO_DATE_COUNT / UpToDateCount step output is emitted (-PassThru returns UpToDateCount). The existing READY_COUNT gate semantics are unchanged.
Step.5 reporting changes. The "All clusters detail" table replaces its Ready boolean column with the same readable Status label, and the "Not-Ready clusters (review first)" table now excludes both Up-to-Date and Ready clusters.
Step.7 "No Clusters Ready" summary now explains why. Add-AzLocalNoReadyClustersStepSummary takes new -UpToDateCount / -NotReadyCount parameters and renders an Up-to-Date vs Not-Ready breakdown table, so an idle apply-updates run makes clear that already-patched clusters are a healthy steady state (and logs an informational notice instead of a warning when every cluster is up to date) and points to the per-cluster Status / Blocking Reasons detail. Both the GitHub Actions and Azure DevOps apply-updates templates wire the two counts through from the readiness gate.
GENERATED_AGAINST_MODULE_VERSION bumped from 0.8.73 to 0.8.74 across all bundled pipeline templates.
v0.8.2 is an operator-experience release for Test-AzLocalApplyUpdatesScheduleCoverage -View Recommend. No public API changes; no output-shape changes that break existing scripts.
Paste-time pain points fixed in the -View Recommend snippet. Two reports on v0.8.1 are addressed:
- The advisor's emitted snippet now embeds a
# All cron times below are UTC ...comment directly aboveschedule:(GH) andschedules:(ADO). Both platforms evaluatecron:in UTC regardless of repo / runner / agent timezone, but operators repeatedly burned time converting from a local-time mental model and ended up firing windows hours offset from their intent. The comment makes the snippet self-documenting once pasted intoStep.6_apply-updates.yml. - The advisor now emits a
> **Indent tip.**blockquote directly above the snippet. The GH snippet is intentionally at 2-space indent (soschedule:is a sibling of the existingworkflow_dispatch:underon:); operators who pasted with the cursor sitting inside the# BEGIN-AZLOCAL-CUSTOMIZE:schedule-triggerscomment block saw VS Code (and JetBrains IDEs) silently double the indent, producing the YAML error "All mapping items must start at the same column". The blockquote explains the cause, instructs the operator to paste at column 0 of a fresh blank line soschedule:lines up withworkflow_dispatch:, and tells them how to recover (delete two leading spaces from every pasted line) if they already hit it.
Module foundations for the upcoming executable-YAML refactor. Five new internal (Private) helpers - Get-AzLocalPipelineHost, Set-AzLocalPipelineOutput, Add-AzLocalPipelineStepSummary, Write-AzLocalPipelineNotice, Write-AzLocalPipelineWarning - abstract over the host-specific quirks of GitHub Actions vs Azure DevOps output channels ($env:GITHUB_OUTPUT vs ##vso[task.setvariable], $env:GITHUB_STEP_SUMMARY vs ##vso[task.uploadsummary], ::notice/::warning vs ##vso[task.logissue]). Not exported and no user-visible effect in v0.8.2 - they are foundations for the upcoming per-Step Export-* / Invoke-* cmdlets that will move inline run: | PowerShell out of the Step.{0..9}.yml files and into the module proper. 23 new Pester assertions cover the three host modes (GitHub auto-detect, Azure DevOps via $env:TF_BUILD, Local fallback) and the byte-identical contracts of each emitted logging-command syntax.
-View Audit NoWindowTag row is now actionable on its own. Pre-v0.8.2 the row showed a generic "Tag clusters with UpdateStartWindow=<days>_<HH:MM>-<HH:MM>" nudge that left operators cross-referencing the matrix CSV to find the offenders. The advisor now names the first 15 affected cluster names grouped by their UpdateRing tag (or (none) when both tags are missing), e.g. Prod -> hci-syd-01, hci-syd-02; Wave1 -> hci-mel-01 (+3 more) (showing first 15 of 42). The row's issue text is clarified to say the tag is optional (the runtime gate only enforces a window when it is set), and the row sorts AFTER Covered (rank 10 instead of 7) so it is treated as informational cleanup rather than a blocker.
Step.3 Allow-list section trimmed in both GH and ADO scaffolds. The Allow-list coverage (schema v2) subsection no longer emits the verbose > Tip - per-ring overrides paragraph + 3-row example block that v0.8.1 produced. Inheriting the top-level allow-list IS the expected steady state for most fleets (install Latest as soon as Ready); the verbose tip was misleading operators into thinking they had to add overrides. The new shape is a one-line steady-state confirmation plus a dedicated ### How to fix - edit $schedulePath subsection (naming the schedule file) and a single-row YAML example showing how to PIN a ring to a specific update (e.g. keep Prod on the latest feature drop only - YY04 / YY10 - and skip cumulative updates between feature drops).
Azure Stack HCI Update Operator custom-role Description rewritten to drop module-internal jargon. The custom-role Description shipped into the Azure RBAC plane (visible in the Azure portal Custom roles blade, in az role definition list / Get-AzRoleDefinition output, and in any RBAC audit/governance tooling) previously read "...read the fleet-connectivity scopes (Arc machines, edge-device NICs, Azure Resource Bridges) required by Step.4" - Step.4 is a module-private concept and meaningless to anyone outside AzLocal.UpdateManagement. The new Description reads "Can read and apply Azure Local cluster updates, manage UpdateRing tags, and read the fleet-connectivity inventory (Arc-enabled machines, edge-device NICs, Azure Resource Bridges) needed to assess pre-update connectivity." Same RBAC grant - Actions[], NotActions[], DataActions[], AssignableScopes[] are byte-identical, so operators with the role already deployed need no action; the next az role definition update will refresh the Description. Updated in lockstep across all 6 sites that ship/document the role: the bundled JSON file plus the inline JSON blocks in Automation-Pipeline-Examples/README.md and docs/rbac.md.
Migration: Install-Module AzLocal.UpdateManagement -Force (or Update-Module). Re-run Test-AzLocalApplyUpdatesScheduleCoverage -View Recommend once to pick up the new self-documenting snippet (the cron values are unchanged from v0.8.1; only the surrounding comment and prose changed).
All 20 bundled Step.{0..9}.yml templates bump GENERATED_AGAINST_MODULE_VERSION from '0.8.1' to '0.8.2'.
See CHANGELOG.md for the full v0.8.2 entry.
v0.8.1 is a docs-and-snippet correctness release. No public API or output-shape changes - existing scripts and pipelines continue to work without modification.
Test-AzLocalApplyUpdatesScheduleCoverage -View Recommend GH-Actions snippet is now copy-paste-safe into Step.6_apply-updates.yml. Pre-v0.8.1 the GitHub-flavoured snippet emitted a full on: + workflow_dispatch: + schedule: block and instructed the operator to "Replace (or merge with) the existing on: block". Step.6_apply-updates.yml already declares workflow_dispatch: with a rich inputs: block (update_ring, update_name, dry_run, raise_itsm_ticket, itsm_config_path, itsm_dry_run, itsm_force_create, module_version) that the manual Run workflow button depends on. Operators who pasted the snippet alongside the existing block ended up with two top-level workflow_dispatch: keys, which GitHub Actions rejects at parse time ('workflow_dispatch' is already defined). The GH snippet now emits ONLY the schedule: block (2-space schedule: indent + 4-space cron indent) so it pastes as-is under the existing on: key, inside the # BEGIN-AZLOCAL-CUSTOMIZE:schedule-triggers / # END-AZLOCAL-CUSTOMIZE:schedule-triggers markers. Azure DevOps snippet shape (top-level schedules:) is unchanged.
Updated "How to fix" prose explicitly tells operators to add (not replace) the schedule: block under the existing on: key, place it inside the BEGIN/END-AZLOCAL-CUSTOMIZE markers so it survives Update-AzLocalPipelineExample refreshes, and explicitly NOT to add a second workflow_dispatch: line.
Four updated AS7-AS10 Pester assertions in Tests/AzLocal.UpdateManagement.Tests.ps1 swap the GH-snippet detector regex from 'workflow_dispatch' to '(?m)^\s*schedule:\s*$' (the singular schedule: key on its own line is GH-Actions-unique; ADO emits the plural schedules: at column 0).
Migration: for operators who have ALREADY pasted the v0.8.0-flavoured snippet into Step.6_apply-updates.yml and seen the duplicate-key error: either Update-AzLocalPipelineExample -Destination <path> to refresh Step.6 and re-run Test-AzLocalApplyUpdatesScheduleCoverage -View Recommend, or manually delete the duplicate empty workflow_dispatch: line from Step.6.
All 20 bundled Step.{0..9}.yml templates bump GENERATED_AGAINST_MODULE_VERSION from '0.8.0' to '0.8.1'.
See CHANGELOG.md for the full v0.8.1 entry.
v0.8.0 is a patch release rolling up three follow-ups to v0.7.99 plus Step.2 UX fixes. No public API changes. Set-AzLocalClusterUpdateRingTag -PassThru gains two new enum values (Action='NoChange' and Status='AlreadyInSync') to distinguish steady-state clusters from genuinely-skipped ones; existing scripts that switch on Created/Updated/Skipped/Failed continue to work, with already-in-sync clusters now surfacing under the new bucket rather than spurious Skipped.
Step.2 Set-AzLocalClusterUpdateRingTag UX cleanup. Three operator-visible fixes:
- The skip branch no longer logs a misleading
Warning+ "use -Force to overwrite" hint for clusters that are already in the desired state. Steady-state clusters now log anInfoline (All managed tags already match desired state - no action needed) and are tallied separately asAlready in sync (no change needed):. TheWarningpath is preserved for the genuine case (UpdateRingdiffers from target AND no schedule diff). -Forceshort-circuits when all managed tags already match desired state - saves a redundant ARM PATCH per cluster on already-clean fleets. Behaviour when there IS something to change is unchanged.- Step.2 GitHub Actions + Azure DevOps job summaries now include a per-cluster result breakdown (counts table + collapsible per-cluster details), at parity with Step.3/5/6/8. Previously the run-level summary only showed the 2-row settings table.
Step.7 criticalElapsedDays form-default fixed 7 -> 3. The v0.7.99 release lowered the CRITICAL overall-elapsed tier from 7 to 3 days. The inline script default ($criticalElapsedDays = 3) and the help-text ("default 3 days") were updated correctly, but the workflow_dispatch input default value (GH) / pipeline-parameter default value (ADO) on the form itself was left at '7'. When an operator triggered the workflow and left the input untouched, the non-empty form-default '7' was passed in via INPUT_CRITICAL_ELAPSED_DAYS, the override branch fired, and the threshold silently reverted to the old 7 days - undoing the v0.7.99 behaviour change.
Step.7 updateRing form-default fixed 'Wave1' -> '' (empty). Aligns Step.7 with Step.8 + Step.9, which already use default: ''. The downstream guard if ($scope -eq 'by-update-ring' -and $updateRing) honours empty as "no filter", so behaviour is unchanged on the default scope=all path. Removes the misleading "Wave1" value that suggested a real tenant-specific default existed when the operator switched to scope=by-update-ring.
New Tests/Pii-Guard.Tests.ps1 repo-hygiene guard. Pester now scans every file under Tests/ on each run and fails the build if it finds emails, .onmicrosoft.com tenant UPN domains, GUIDs in identity contexts (tenantId / clientId / objectId / principalId / applicationId), or real public IPv4 addresses outside an explicit allow-list. RFC1918 / loopback / link-local / CGNAT / RFC5737 doc / multicast / subnet-mask shapes / well-known public DNS are auto-excluded. Catches accidental PII commits to a public repo at PR time rather than after-the-fact.
Publish-Module.ps1 excludes docs/RELEASE-PROCESS.md from the published .nupkg. That file is a maintainer-only release checklist (its own opening line states "Consumers do not need to read it") and has no runtime value. Repo copy on GitHub stays for maintainer reference; consumers no longer ship it inside every installed module folder.
All 20 bundled Step.{0..9}.yml templates bump GENERATED_AGAINST_MODULE_VERSION from '0.7.99' to '0.8.0'.
See CHANGELOG.md for the full v0.8.0 entry.
v0.7.99 is a breaking-rename release for the readiness / fleet-status cmdlets plus a Step.7 default tightening and an artifact-naming cleanup. All 20 bundled pipeline YAMLs were updated to consume the new output shapes.
Property renames. Get-AzLocalUpdateSummary.AvailableUpdatesCount -> ActionableUpdatesCount (the column only ever counted Ready + NotReady, not the full inventory). Get-AzLocalClusterUpdateReadiness.AvailableUpdates -> AllAvailableUpdates. Get-AzLocalFleetStatusData.AvailableUpdates -> AllAvailableUpdates. Same content, clearer names. The cmdlet name Get-AzLocalAvailableUpdates is unchanged.
Readiness Summary: 2-bucket -> 3-bucket model. Get-AzLocalClusterUpdateReadiness's console block and JSON export both gain a Up to Date bucket distinct from Not Ready for Update. Previously clusters with UpdateState in (UpToDate, AppliedSuccessfully) and no remaining available updates were rolled into the NotReady total, which falsely flagged a healthy fleet as needing attention. Console labels: Ready for Update / Up to Date (new) / Not Ready for Update. JSON Summary keys: ClustersReadyForUpdate / ClustersUpToDate (new) / ClustersNotReadyForUpdate.
Step.5 + Step.8 pipelines updated (both GH Actions and Azure DevOps) to compute and surface the 3-bucket model and to consume the renamed AllAvailableUpdates property. readiness-status.json Summary keys move to ReadyForUpdate / UpToDate / NotReadyForUpdate. Automation-Pipeline-Examples/.itsm/azurelocal-itsm.yml is unchanged - the NotReady key there maps a per-run Status value from Start-AzLocalClusterUpdate.ps1, which is a separate (and stable) contract.
Step.7 CRITICAL elapsed-days default 7 -> 3. Skull (rotating-light) threshold (2x CRITICAL) consequently lowers from 14 to 6 days. Override via criticalElapsedDays: <N> workflow_dispatch / pipeline param.
Step.7 testsuite-level time= zeroed. Per-testcase time= continues to carry real per-run elapsed seconds (v0.7.98 behaviour preserved). The suite-level wrapper now emits time="0" rather than summing five unrelated wall-clock ages.
Artifact zip names now carry the producing Step number. Every bundled template's artifact zip moves from azlocal-<purpose>_yyyyMMdd_HHmmss to azlocal-step.X-<purpose>_yyyyMMdd_HHmmss. The Pester guard for the artifact-name convention now requires the ^azlocal-step\.\d+- regex shape.
Docs. Automation-Pipeline-Examples/README.md section 1.1 Step.N table gains direct GH Actions + Azure DevOps YAML column links; section 9 (ThrottleLimit) trimmed of stale v0.7.68 historical narrative. docs/cmdlet-reference.md reflects the property and Summary renames.
All 20 bundled Step.{0..9}.yml templates bump GENERATED_AGAINST_MODULE_VERSION from '0.7.98' to '0.7.99'.
See CHANGELOG.md for the full v0.7.99 entry.
v0.7.98 is a pipeline-template UX release. The Step.7 in-flight update monitor pipelines (Step.7_monitor-updates.yml in both github-actions/ and azure-devops/) get a UX overhaul so stuck and critical runs surface immediately, and the Step.7 + Step.8 JUnit emitters now populate time= with real run elapsed seconds. No public-cmdlet changes.
Step.7 UX overhaul. Each row is now scored on a composite SeverityScore (StepError severity x 1000 + RunSeverity x 100 + elapsed-hours bucket), the job summary opens with a single CRITICAL / WARN / OK fleet status badge (e.g. CRITICAL - 1 stuck step error(s), 1 run(s) > 14d, 1 step(s) > 4h), each row carries per-cell StateIcon + StatusIcon icons and a horizontal chip stack of flags (STEP-STUCK, RUN-STUCK, UNRESOLVED, RECENT-FAIL), and each failed step's errorMessage is wrapped in a collapsible <details><summary>Verbose error</summary>...</details> block.
JUnit time= populated (Step.7 + Step.8). Previously every <testcase> and <testsuite> emitted time="0", which made GitHub Test Reporter render "5 tests were completed in 0ms". Step.7 now computes per-run elapsed seconds (end - start for completed / failed, current elapsed for in-flight). Step.8's Update Run History and Error Details testsuite now emits time = DurationMinutes * 60 per row. Other Step.8 testsuites (FleetVersionDistribution, AzureLocalFleetUpdateStatus) intentionally stay at time="0" - they are instantaneous snapshot projections.
All 20 bundled Step.{0..9}.yml templates bump GENERATED_AGAINST_MODULE_VERSION from '0.7.97' to '0.7.98'.
See CHANGELOG.md for the full v0.7.98 entry.
v0.7.97 is a documentation-only follow-up to v0.7.96. The three Markdown files that ship inside the published PSGallery .nupkg under the module folder were refreshed to mirror the v0.7.96 module behaviour. Consumers who installed v0.7.96 fresh saw stale Step.7 + Step.8 narrative in those in-package docs because they were updated AFTER Publish-Module.ps1 ran. v0.7.97 republishes the package with the aligned docs.
No code anywhere in the module, no inline-script changes in any Step.{0..9}.yml template. Only the GENERATED_AGAINST_MODULE_VERSION pin moves from '0.7.96' to '0.7.97' across all 20 bundled templates (10 GitHub Actions + 10 Azure DevOps).
Automation-Pipeline-Examples/README.md(CI/CD runbook): section-1 Step.7 bullet now mentions the newStatus+ErrorMessagecolumns, theStepErrorJUnit failure type, the always-shown Failed-runs block, portal-linked Cluster / Update Name cells, and the newSTEP_ERRORED/UNRESOLVED_FAILURESGITHUB_OUTPUTvalues. Step.8 bullet now mentionsNeedsAttentionpromotion into theUpdate Failedbucket, the newAction Requiredbucket forPreparationFailed, andPreparationInProgressfolding intoUpdate In Progress.Automation-Pipeline-Examples/docs/appendix-pipelines.md(per-step pipeline reference): Step 7 and Step 8 tables refreshed: thePurpose,Artefacts,Exit conditions, andIntroducedrows now describe the new columns, JUnit failure types, the bucket cascade, theprimaryActionRequiredJUnit attribute,ACTION_REQUIREDoutput,Summary.UpdateFailures+Summary.ActionRequiredJSON keys, and the portal-linked markdown cells. A duplicateExit conditionsrow on Step 7 was removed.docs/release-history.md(this file): current-release pointer bumped from v0.7.89 to v0.7.97; the v0.7.96 entry was already prepended in PR #66 (commitc395ed3).- All 18 bundled
Step.{0..9}.ymltemplates bumpedGENERATED_AGAINST_MODULE_VERSIONfrom'0.7.96'to'0.7.97'. Pin-only; no inline-script content changes.
This release also closes a process gap: the in-package docs (the three files listed above) ship inside the PSGallery .nupkg because Publish-Module.ps1 only strips Tests/, Tools/, Module-reviews/, and root-level non-README *.md. The Automation-Pipeline-Examples/ tree and the docs/ folder ride along with the manifest, so any time their content is updated AFTER a publish, a republish is required for consumers to see the aligned narrative. v0.7.97 also adds this verification to the maintainer-facing docs/RELEASE-PROCESS.md and the version-bump checklist.
v0.7.96 is a focused operator-visibility release driven by a side-by-side comparison of the bundled Step.7 + Step.8 pipelines against the Azure portal Update Manager view used by the Azure Local product team. Three gaps were closed in lock-step: the in-flight monitor (Step.7 monitor-updates) now surfaces the Status column and the deepest nested errorMessage for every in-flight run, plus a new StepError JUnit failure type that fires on stuck-but-still-fresh runs (the Arizona-shaped case where a step has been in Status='Error' for 19+ days but the run's overall elapsed time is still under long_running_threshold_hours); the daily snapshot (Step.8 fleet-update-status) reworks the Primary Status bucket cascade to align with the Azure portal Update Manager state filter by promoting NeedsAttention into the Update Failed bucket, adding a new Action Required bucket for PreparationFailed (with its own actionable guidance because the run never started so re-arming is required), and folding PreparationInProgress into Update In Progress; and both pipelines now render Cluster Name and Update Name cells in their markdown summaries as <a href="https://portal.azure.com/..."> deep-links to the cluster's Updates blade and the single-instance update-run history view, matching the Azure portal's hyperlink behaviour.
New Status + ErrorMessage columns on every update run. Format-AzLocalUpdateRun now emits both columns derived from properties.progress.status and the deepest errorMessage walked from the nested properties.progress.steps[] tree (uses a coalesce(e9Msg..e1Msg) recursion via the new private Get-DeepestErrorMessage helper that walks all 9 nesting levels of the ARM steps tree). The columns appear in update-monitor.csv, the in-flight markdown table, the always-shown Failed-runs block, and the update-runs.csv artefact emitted by Step 8.
Step.7 new StepError JUnit failure type. Runs whose deepest step status is Error but whose overall elapsed time is below long_running_threshold_hours (default 6h) now generate a JUnit <failure failureType='StepError'> entry so they surface in the Checks tab WITHOUT needing the long-running threshold to fire. Two new GITHUB_OUTPUT values support this: STEP_ERRORED (count of runs that hit a step error inside the threshold) and UNRESOLVED_FAILURES (count of Failed runs surfaced from Get-AzLocalUpdateRunFailures). The always-shown Failed-runs block in the Step.7 markdown summary carries an MS Learn TSG link.
Step.8 new Action Required Primary Status bucket. The Primary Status bucket cascade is now Update Failed > Action Required > Health Failure > SBE Prerequisite Blocked > Update In Progress > Ready for Update > Up to Date > Needs Investigation. PreparationFailed lands in the new Action Required row with separate actionable prose. Surfaces via: (1) new <property name="primaryActionRequired" value="$stActionRequired"/> JUnit attribute on the primary testsuite; (2) per-testcase failureType='PreparationFailed' instead of the generic UpdateFailure; (3) new ACTION_REQUIRED GITHUB_OUTPUT (actionRequired pipeline variable in ADO); (4) Summary.UpdateFailures + Summary.ActionRequired rolled-up bucket counts in readiness-status.json; (5) new "Action Required (PreparationFailed)" row in the Primary Status markdown table with its own callout.
Portal deep-links in both Step.7 and Step.8 markdown tables. Cluster Name cells render as <a href="https://portal.azure.com/#@/resource{ClusterResourceId}/updates">ClusterName</a> and Update Name cells render as <a href="https://portal.azure.com/#view/Microsoft_AzureStackHCI_PortalExtension/SingleInstanceHistoryDetails.ReactView/resourceId/{enc}/updateName/{name}/updateRunName/{runId}/refresh~/false">UpdateName</a>. Verified working in the Arizona + Orlando smoke tests this release cycle.
8 new Pester tests added (4 covering Format-AzLocalUpdateRun Status + ErrorMessage emission, 4 covering Get-DeepestErrorMessage recursive walk semantics including the L3-deepest case used in production). Full suite green at 794 / 0 / 1. All 18 bundled Step.{0..8}.yml templates bumped GENERATED_AGAINST_MODULE_VERSION from '0.7.95' to '0.7.96'; the GH Step.8 run: block measures 17,877 chars (under the repo's 18,000-char ceiling guard).
v0.7.88 was a focused Step.8 step-summary readability polish. The bundled Step.8 fleet-health-status GitHub Actions and Azure DevOps pipelines now render the per-cluster fleet rollup right below the KPI summary - above the failure breakdown - so an operator triaging unhealthy clusters sees Health status, Update Status, current version, SBE version, Azure Connection state, last-check timestamp + age and node count at a glance, then dives into Health Check Failures By Reason and Detailed Results for context. The fleet-rollup column that used to be labelled Age (days) was renamed to Health Check Age (days), which is unambiguous next to the Last Checked timestamp column - the value is the age in days of the cluster's last 24-hour Azure Stack HCI health-check result (sourced from Get-AzLocalFleetHealthOverview.HealthResultsAgeDays), with negative values continuing to mean "no LastChecked timestamp at all".
Step.8 markdown section order. Both GH + ADO pipelines now emit: KPI summary table -> ### Fleet Health Overview (fleet rollup) -> ### Health Check Failures By Reason (most widespread first) -> ### Detailed Results (per-cluster, per-failure) -> ### Reports Available. Previously the fleet rollup sat at the very end of the summary, requiring operators to scroll past every failure row to reach the per-cluster overview.
Step.8 fleet-rollup column rename. Age (days) -> Health Check Age (days). The previous label was ambiguous (could plausibly mean cluster age, node age, current-version age, or update-readiness age); the new label is unambiguous and matches the underlying Get-AzLocalFleetHealthOverview.HealthResultsAgeDays data source.
All 18 bundled Step.{0..8}.yml templates (9 GitHub Actions + 9 Azure DevOps) bumped GENERATED_AGAINST_MODULE_VERSION from '0.7.87' to '0.7.88'. Only Step.8_fleet-health-status.yml had inline-script content changes; the other 17 were pin-only bumps.
v0.7.87 shipped a focused architectural hardening of the Step.4 Fleet Connectivity Status pipeline. The 285-line markdown renderer that previously lived inline as a ~22 KB pwsh run: body in both the GitHub Actions and Azure DevOps Step.4 YAML templates was extracted into a new public module function. This gave both pipelines a single source of truth, made the markdown layout unit-testable in Pester, and kept every bundled run: body comfortably below the GitHub Actions 21,000-char expression-length cap. A new Pester regression test enforces an 18,000-char ceiling on every run: (and script:) block in every bundled GitHub Actions YAML so any future regrowth is caught in CI before it can hit production parsing.
New public function New-AzLocalFleetConnectivityStatusSummary. A pure markdown renderer that consumes the seven CSV reports produced earlier in the Step.4 pipeline by Get-AzLocalFleetConnectivityStatus (or the equivalent in-memory object arrays via the FromObjects parameter set) plus an explicit -Counts hashtable of KPI totals. It is a pure renderer over already-collected data - no Azure CLI, no Resource Graph extension, no Az.* modules required, no re-querying Azure. Returns the markdown string by default; -OutputPath writes UTF-8 no-BOM and -PassThru lets you combine both. ASCII source with Unicode emoji icons emitted via [char]0xNNNN. The function shipped with comprehensive Pester unit-test coverage (parameter validation, required-key validation on -Counts, markdown structure, empty-input placeholders, orphan-ARB section conditionality, multi-cluster-per-RG ARB matching, output-path semantics, CSV-based input with graceful missing-CSV handling).
Step.4 GH + ADO YAML refactor. The 283-line, 20,460-char inline renderer in github-actions/Step.4_fleet-connectivity-status.yml was replaced with a 23-line body that calls New-AzLocalFleetConnectivityStatusSummary. The file shrunk from 863 to 603 lines and the renderer step's run: body became ~1,083 chars - a 95% reduction. The Azure DevOps twin (azure-devops/Step.4_fleet-connectivity-status.yml) got the equivalent refactor so both pipelines call the same module function. As a side-effect this fixed a pre-existing structural anomaly in the ADO YAML: the Display Summary step's pwsh literal block scalar had silently absorbed the trailing displayName: / condition: / env: keys AND the subsequent PublishTestResults@2 task (because those lines sat at indent 12-16 while the block scalar base indent was 8). The parsed ADO pipeline used to have only 10 steps; in v0.7.87 it had 11 steps, with Display Fleet Connectivity Summary and Publish Fleet Connectivity JUnit Diagnostics now real, distinct ADO tasks that actually run.
21K-cap regression guard in Pester. A new Regression v0.7.87: bundled GitHub Actions YAML run: blocks stay under GitHub 21,000-char expression cap Describe block enumerates every run: and script: literal block scalar in every bundled github-actions/Step.{0..8}.yml template and asserts each is below 18,000 chars. The 3K headroom under the 21K cap covers future feature growth and possible future re-introduction of ${{ }} substitutions (the cap applies to the post-substitution length of any run: body that contains at least one ${{ }}). Two additional asserts verify both the GH and ADO Step.4 YAMLs CALL the new renderer function and do NOT inline the ## Fleet Connectivity Status Summary heading.
All 18 bundled Step.{0..8}.yml templates (9 GitHub Actions + 9 Azure DevOps) bumped GENERATED_AGAINST_MODULE_VERSION from '0.7.86' to '0.7.87'.
v0.7.86 was a documentation follow-up to v0.7.85 - no code changes anywhere in the module, no YAML inline-script changes. While reviewing PR #56 before merging v0.7.85, the Automation-Pipeline-Examples README + appendix were found to still describe the seven-pipeline layout from before Step.4 Fleet Connectivity Status was inserted in v0.7.79 (and before Step.0 Authentication Test was added in v0.7.70). The follow-up audit also caught two related cross-doc gaps: the ITSM/README.md setup guide had never been updated when Step.4 shipped opt-in ServiceNow ticketing in v0.7.76, and the CI/CD Automation-Pipeline-Examples/README.md had no direct cross-link to the top-level docs/concepts.md / docs/rbac.md / docs/troubleshooting.md references. The bundled YAMLs themselves were correct; only the human-facing setup runbooks were stale. v0.7.86 republished the module with the corrected docs: section 1.1 mapping table re-rendered so Step.4 = Fleet Connectivity Status (off-by-one row labels corrected for Step.5..Step.8); section 6.6 fleet-monitoring narrative now covers all three daily steady-state pipelines (Step.4 + Step.7 + Step.8) including the v0.7.85 reconciliation enhancements and the four ARM/ARG scopes Step.4 reads; section 13 file layout re-listed in Step.0..Step.8 numeric order with descriptive comments + cron schedules; Automation-Pipeline-Examples/docs/appendix-pipelines.md renumbered from A.1..A.7 to A.0..A.8 so the appendix section number always matches its Step.N_*.yml filename, with two new sections added (A.0 Authentication Validation and Subscription Scope Report and A.4 Fleet Connectivity Status); ITSM/README.md updated to list four ITSM-wired pipelines (Step.4 alongside Step.6/Step.7/Step.8) including a v0.7.76 callout documenting the Step.4 wiring and per-resource UpdateName patterns. All 18 bundled Step.{0..8}.yml templates bumped GENERATED_AGAINST_MODULE_VERSION from '0.7.85' to '0.7.86' (pin-only; no inline-script changes).
v0.7.85 was a documentation enhancement for the Step.4 fleet-connectivity step-summary - no code changes in Get-AzLocalFleetConnectivityStatus itself. After v0.7.84 fixed the underlying correctness bugs, a production operator running Step.4 against a 20-cluster fleet saw Node coverage delta = -6 (cluster-reported = 38, Arc-tagged = 44) and asked what it meant. The previous label Arc-joined physical nodes (implying a join was computed) plus a one-directional caption (which only described positive-delta causes) made it harder than necessary to interpret. v0.7.85 made the table self-explanatory and actionable so operators could act on non-zero numbers without external context.
Column rename + Notes column rewrites in the Node + ARB Coverage Reconciliation table. Arc-joined physical nodes -> Arc-tagged physical nodes. The previous label was a misnomer: this value has never been a join against cluster.reportedProperties.nodes - it is the raw count of microsoft.hybridcompute/machines carrying properties.detectedProperties.cloudprovider=AzSHCI and kind!=HCI. The Notes column now explicitly calls out that the two counts come from independent KQL queries and can legitimately disagree in EITHER direction. Stale wording on Cluster-reported node count (sum) that still referenced the pre-v0.7.84 non-existent nodeCount scalar has been replaced with the correct Sum of array_length(properties.reportedProperties.nodes) across all clusters. The Node coverage delta Notes are now BIDIRECTIONAL: POSITIVE = clusters claim more nodes than Arc has (Arc-onboarding lag / deleted Arc resource / missing AzSHCI provider tag / stale cluster.reportedProperties.nodes array); NEGATIVE = Arc has more AzSHCI-tagged machines than clusters claim (orphan/decommissioned Arc resource / pre-staged-but-not-yet-joined node / mis-tagged non-cluster machine).
New ### How to interpret + act on a non-zero reconciliation subsection appended after the table. Per-direction remediation lists for Node coverage delta POSITIVE (Arc-onboarding lag -> wait + re-run; Arc resource deleted -> azcmagent connect; missing AzSHCI tag -> re-run onboarding extension; stale cluster.reportedProperties.nodes -> compare against Get-ClusterNode) and NEGATIVE (orphan/decommissioned Arc resource -> delete or re-attach; pre-staged node -> wait for commissioning; mis-tagged machine -> clear the tag). Includes an inline Resource Graph query template to enumerate the specific Arc machines causing a NEGATIVE node coverage delta. Also documents Clusters without an ARB > 0 causes (ARB never deployed / ARB deleted but cluster present / RP not registered) and Orphan ARBs > 0 causes (cluster deleted but ARB not cleaned up / scope-list drift / ARB for an excluded cluster).
Applied identically to both bundled Step.4 YAMLs (Automation-Pipeline-Examples/github-actions/Step.4_fleet-connectivity-status.yml and Automation-Pipeline-Examples/azure-devops/Step.4_fleet-connectivity-status.yml); all 18 bundled Step.{0..8}.yml templates bumped GENERATED_AGAINST_MODULE_VERSION from '0.7.84' to '0.7.85'. Only Step.4 had inline-script content changes; the other 17 were pin-only bumps.
v0.7.84 was a HOTFIX for 3 read-path correctness bugs in Get-AzLocalFleetConnectivityStatus (the cmdlet that powers the Step.4 fleet-connectivity summary), surfaced against a real 20-cluster fleet via the same GitHub Actions Step.4 run on 2026-05-21 that validated the v0.7.83 hotfix. Bug A - the cluster Nodes column was 0 for every cluster (and Node coverage delta therefore equalled -(Arc-joined node count)) because the code read properties.reportedProperties.nodeCount which does NOT exist in the Azure Local cluster ARG schema; the correct property is the properties.reportedProperties.nodes ARRAY. Bug B - the Non-Connected Machines table ClusterName column was corrupted to a single character (cluster Mobile -> e, alrs-cc -> c, etc.) because [string]($clusterId -split '/') joined the array elements with spaces and [-1] then returned the last CHARACTER of that joined string instead of the last array element (same scalar-collapse bug class as v0.7.82/v0.7.83). Bug C - the Azure Resource Bridges table DaysSinceLastModified was -1 for EVERY ARB because (1) ARG's default resources response often stripped systemData (the KQL now explicitly extends lastModifiedAt) and (2) Running ARBs were short-circuited to -1 as an opaque sentinel (the short-circuit was dropped so real days is shown for all ARBs; -1 is now reserved only for genuinely missing/unparseable timestamps). A new Pester Regression v0.7.84 Describe block added 10 tests across 3 contexts: static regex guards on the source file detect regression of any of the three fixes, execution tests mock realistic ARG payloads (3-node Mobile cluster, Disconnected Arc machine with real ARM-ID parentClusterResourceId, Running + Offline ARBs with systemData.lastModifiedAt set 5/10 days ago), and a negative-control test re-executes the pre-fix [string](array)[-1] shape and proves it still returns 'e' for cluster Mobile. Also new: cross-call ARG throttle cooldown in Invoke-AzResourceGraphQuery - complements the v0.7.68 per-page retry loop with cross-call coordination so a throttle event observed in one call causes subsequent calls to voluntarily sleep out a short cooldown window on entry. Cooldown duration scales with -RetryBaseSeconds (capped at 10s) and the consecutive-throttle counter decays by 1 on every clean call. New $script:LastResourceGraphCrossCallCooldownSeconds diagnostic and -DisableCrossCallCooldown switch; Pester Cross-call throttle coordination (v0.7.84) Context added 4 tests. The existing v0.7.79 cluster mock fixture used the wrong-named scalar nodeCount = 2 and had silently masked Bug A in unit tests for multiple releases; corrected to the realistic nodes = @(...) array shape. All 18 bundled Step.{0..8}.yml templates bumped GENERATED_AGAINST_MODULE_VERSION from '0.7.83' to '0.7.84' (no inline-script changes outside the cmdlet itself). Known follow-up (v0.7.85): production review of the corrected v0.7.84 Step.4 output revealed that the Node + ARB Coverage Reconciliation table Notes column described the delta as one-directional and labelled the Arc count as Arc-joined physical nodes (implying a join was computed). v0.7.85 reworded the table to be bidirectional and added a ### How to interpret + act on a non-zero reconciliation subsection - see v0.7.85 release notes.
v0.7.83 was a HOTFIX for a runtime crash in the Step.4 fleet-connectivity ARB JUnit-XML generation that shipped in v0.7.82. When an Azure Resource Bridge failure case had a single (non-comma-separated) ClusterId, the inline script blew up with Method invocation failed because [System.Char] does not contain a method named 'Trim'. The buggy two-line pattern $clusterIdList = if ($r.ClusterId) { @(($r.ClusterId -split ',\s*') | Where-Object { $_ }) } else { @() }; $clusterIdList[0].Trim() hit a PowerShell collection-unwrap gotcha: when Where-Object yielded a single scalar, the @() wrap was silently undone by the if-as-expression, $clusterIdList collapsed to a bare [string], indexing returned [char], and .Trim() threw at runtime. Multi-cluster RG ARBs (comma-separated ClusterId) were unaffected, which is why this only surfaced in production against a real customer fleet. Fixed in both Step.4 YAMLs by adding a [string[]] cast on $clusterIdList (which forces array shape regardless of element count) plus a defence-in-depth [string] cast at the [0] indexing site. The same if-@-else-@() shape existed twice more per file in the orphan-ARB reconciliation block; both call sites were also [string[]]-cast. A new Pester regression block Regression v0.7.83: Step.4 ARB inline script handles single-cluster ClusterId without [char].Trim() bug (9 tests) re-executed the exact buggy pattern against single-cluster, multi-cluster, null, and empty ClusterId payloads, plus a negative-control test that proved the pre-fix shape still threw. All 18 bundled Step.{0..8}.yml templates bumped GENERATED_AGAINST_MODULE_VERSION from '0.7.82' to '0.7.83' (no code changes outside Step.4). Known issue (fixed in v0.7.84): production validation of the v0.7.83 hotfix surfaced 4 additional read-path correctness bugs in Get-AzLocalFleetConnectivityStatus - see v0.7.84 release notes for details.
v0.7.82 packaged the custom RBAC role definition as a bundled JSON file so operators no longer needed to copy-paste from the README. The new file Automation-Pipeline-Examples/azlocal-update-management-custom-role.json contains the canonical Azure Stack HCI Update Operator role definition (13 actions: the 12 used by current cmdlets plus Microsoft.HybridCompute/machines/extensions/read reserved for future Arc-machine extension reporting). Automation-Pipeline-Examples/README.md Section 4.1 was updated to point at the bundled file as the first install path. A new callout in Section 4.1 and docs/rbac.md flagged the JSON-shape difference between the CLI / PowerShell format (az role definition create, New-AzRoleDefinition) and the Azure portal Edit-a-custom-role JSON tab format (ARM resource representation wrapped in properties), so operators do not hit Malformed JSON: "properties" property not present when pasting into the portal. The same callout also flagged a UTF-8 BOM gotcha: az's Python JSON parser rejects BOM-prefixed files with Expecting value: line 1 column 1 (char 0); the shipped file was BOM-free. All 18 bundled Step.{0..8}.yml templates bumped GENERATED_AGAINST_MODULE_VERSION from '0.7.81' to '0.7.82'. Known issue (fixed in v0.7.83): the multi-cluster-RG ARB handling added to Step.4 in v0.7.82 introduced a [System.Char] does not contain a method named 'Trim' crash on single-cluster ARBs - see v0.7.83 release notes for details.
v0.7.81 was a documentation-only release focused on the CI/CD permission guidance. The previous Automation-Pipeline-Examples/README.md presented the least-privilege custom role (Azure Stack HCI Update Operator) and the built-in Azure Stack HCI Administrator role as roughly equal "Option A / Option B" choices, biasing readers toward the over-permissive built-in role for labs. v0.7.81 pivoted every permission code path to custom-role-first for every environment; the built-in role was framed as a temporary fallback for tenants where the operator cannot create custom roles, hidden behind <details> / commented-out alternatives. Sections 3.1 Step 2, 3.2 (ADO), 3.3 (Managed Identity), 3.4 (SP+secret), section 4 intro, section 4.2 and section 11 were reframed; section 4.1 gained an expanded Migration tip (built-in -> custom role, no downtime) block including the az role assignment delete cleanup command. All 18 bundled Step.{0..8}.yml templates bumped GENERATED_AGAINST_MODULE_VERSION from '0.7.80' to '0.7.81'. No module code changes.
v0.7.80 was a documentation-fix release. The custom RBAC role definition in docs/rbac.md (used by Step.4 pipeline service principals) was missing three reads that Get-AzLocalFleetConnectivityStatus (added in v0.7.79) needs: Microsoft.HybridCompute/machines/read, Microsoft.AzureStackHCI/edgeDevices/read, and Microsoft.ResourceConnector/appliances/read. Without them the cmdlet still returns the cluster connectivity section but every other section (Arc agents, physical NICs, Azure Resource Bridges) silently returned zero rows because ARG yields an empty .data array for resource types the caller cannot read. The doc also gained an "Updating an existing custom role" sub-section so an az role definition update keeps the existing role GUID and assignments. No module code changes; pipeline pin bumped from '0.7.79' to '0.7.80'.
v0.7.79 enabled the Step.5 daily readiness check out of the box. The schedule: cron block in Step.5_assess-update-readiness.yml (GitHub Actions and Azure DevOps) was previously commented out; it is now active at 07:00 UTC daily. v0.7.79 also introduced Get-AzLocalFleetConnectivityStatus, a module cmdlet that replaces the inline ARG queries previously embedded in Step.4 YAMLs and returns a single [PSCustomObject] with the seven fleet-connectivity scopes (ClusterRows, ArcSummary, NonConnectedMachines, NicIssues, NicAll, NicStats, ArbRows). No module code changes for the schedule itself; pipeline pin bumped from '0.7.78' to '0.7.79'.
v0.7.78 is a targeted hotfix release focused on Step.4 fleet-connectivity data-shape resilience. This fixes the regression where totals were populated but key fields rendered blank in markdown and JUnit (ClusterName, AgentStatus, NicName, ArbName). Invoke-ArgQuery now normalizes both ARG response shapes (object rows and tabular columns + data rows), Step.4 projections use coalesce(...) for non-empty key fields, and JSON export depth is raised to reduce truncation warnings.
v0.7.77 was a small pin-bump release. The bundled Step.{0..8}.yml templates (18 files: 9 GitHub Actions + 9 Azure DevOps) bump GENERATED_AGAINST_MODULE_VERSION from '0.7.76' to '0.7.77'. The pin is drift-detection only; Install-Module -Force still installs PSGallery latest at runtime.
v0.7.76 renamed every exported function and private helper from -AzureLocal* to -AzLocal* to align with the module name and the standard PowerShell module-prefix convention. There are no deprecation aliases (the module is pre-1.0 and has no published external consumers). Search-and-replace -AzureLocal -> -AzLocal in any pinned scripts.
Invoke-AzResourceGraphQuery used return , $allRows.ToArray() (the unary-comma trick that keeps the return shape as a single Object[N] through pipeline enumeration). 24 callers wrapped the call with @(Invoke-AzResourceGraphQuery ...) which collected the function's pipeline output (one wrapper containing the inner array), producing Object[1] containing Object[N]. Downstream property access then did PowerShell member enumeration and returned arrays-of-strings, so a 136-row update-run result collapsed into a 1-row ClusterName_=Object[] mess. Fixed by converting all 24 callers to direct assignment ($x = Invoke-AzResourceGraphQuery ...).
Test-AzLocalClusterHealth and the private Get-HealthCheckFailureSummary now dedup byte-identical ARM healthCheckResult rows. ARM upstream was observed emitting two byte-identical rows for the same logical health-check finding (same CheckName, Severity, Description, Remediation, TargetResourceName, Timestamp), doubling the displayed CriticalCount. Dedup is by the COMPLETE row tuple (HashSet[string] keyed on ClusterName | CheckName | Severity | Description | Remediation | TargetResourceName | Timestamp joined by U+001F UNIT SEPARATOR), so per-node distinct findings with different targetResourceName (e.g. UserStorage_1-Repair vs UserStorage_2-Repair) stay separate.
- Finding 2 (test gaps): new Pester cases for row-collapse, ARM dedup (identical / distinct target / empty), KQL arg-length safety.
- Finding 3 (SP secret leak):
Connect-AzLocalServicePrincipalwrites the secret to a temp file with restricted ACL, passes viaGet-Content, removes it in afinallyblock. - Finding 4 + 5 (docs): main README cut from 3372 to ~600 lines; new
docs/tree:cmdlet-reference.md,concepts.md,rbac.md,troubleshooting.md,release-history.md. Pipeline README appendices also extracted toAutomation-Pipeline-Examples/docs/. - Finding 6 + 9 (.psm1): dead-code removed; deliberate
Set-StrictMode -Version 1.0documented at top of file. - Finding 8 (review archive): internal review notes moved into a gitignored
docs/location.
- The main README is the entry point; cmdlet / RBAC / release deep-dives live under
docs/. - Only the current version's What's-New stays inline; older entries live in
docs/release-history.md. - Each release tag MUST run the demote-to-history step.
- Day-2 pipeline operators land on
Automation-Pipeline-Examples/README.md; depth lives inAutomation-Pipeline-Examples/docs/. - ITSM has its own README under
ITSM/.
v0.7.75 was a hardening release on top of v0.7.74. v0.7.74 patched the Test-AzLocalApplyUpdatesScheduleCoverage cross-platform-noise bug at the yml layer by adding -Platform GitHubActions / -Platform AzureDevOps arguments to the bundled Step.3 yml templates - but that fix only took effect for consumers who refreshed their yml via Update-AzLocalPipelineExample. Consumers whose Step.3 yml was a verbatim pre-v0.7.74 copy still saw both the GitHub Actions snippet AND the Azure DevOps snippet in their Step Summary, because their yml did not pass -Platform and the cmdlet's default was -Platform Both. v0.7.75 closed that gap by adding the same auto-selection at the cmdlet layer so stale yml self-heals at runtime. Pipeline pin bumps to '0.7.75'; refresh existing copies via Update-AzLocalPipelineExample.
Test-AzLocalApplyUpdatesScheduleCoverage auto-detects the CI host platform when -Platform is omitted
When the caller does not bind -Platform, the cmdlet inspects the well-known CI environment variables and self-selects:
$env:GITHUB_ACTIONS -eq 'true'->-Platform GitHubActions(emit only the GitHub Actions snippet)$env:TF_BUILD -eq 'True'OR any non-empty$env:SYSTEM_TEAMFOUNDATIONCOLLECTIONURI->-Platform AzureDevOps(emit only the Azure DevOps snippet)- Neither set (interactive operator-at-workstation) -> existing
-Platform Bothdefault preserved (emit both side by side)
Detection is gated on $PSBoundParameters.ContainsKey('Platform') so an explicit caller value (including an explicit -Platform Both) always wins over the auto-detect.
Four new Pester tests (AS7-AS10) in the existing Test-AzLocalApplyUpdatesScheduleCoverage Describe verify all four auto-detect cases: GH-only when $env:GITHUB_ACTIONS='true' + no -Platform; ADO-only when $env:TF_BUILD='True' + no -Platform; both-when-explicit -Platform Both overrides auto-detect; both-when-no-env-vars preserves the interactive default. BeforeEach / AfterEach blocks clear the CI env vars so the tests are hermetic.
All 14 Step.{1..7}.yml files (7 GitHub Actions + 7 Azure DevOps) bumped GENERATED_AGAINST_MODULE_VERSION from '0.7.74' to '0.7.75'. Refresh via:
Update-AzLocalPipelineExample -Destination .\.github\workflows -Platform GitHub
Update-AzLocalPipelineExample -Destination .\.azure-pipelines -Platform AzureDevOpsAll v0.7.75 changes were backward compatible.
v0.7.74 is a bug-fix + UX release on top of v0.7.73. It addresses two distinct findings raised against v0.7.73 against the same live 20-cluster fleet: (1) Get-AzLocalFleetHealthOverview started failing with a KQL ParserFailure: token=<EOF> at character 2757 because the v0.7.73 query growth crossed the az graph query -q argument-truncation threshold (~2.8 KB wire-side); (2) the Step.3 - Apply-Updates Schedule Coverage Audit recommendation block was reported as "very hard to follow and understand what to do" - it was a four-section advisory but lacked an explicit fix-in-this-order checklist, did not explain why each finding mattered, did not include a before/after YAML snippet for missing rings, and shipped a commented-out cron block that operators were copy-pasting verbatim including the # prefixes. Both are fixed. Pipeline pin bumps to '0.7.74'; refresh existing copies via Update-AzLocalPipelineExample.
The v0.7.73 change (HealthStatus normalisation via case()) added ~250 chars of KQL projection, and the v0.7.73 commit also added a six-line // KQL comment block above the projection (~470 chars). The combined wire query grew from ~2400 chars (v0.7.72 baseline) to 3115 chars. On Windows the Azure CLI's az graph query -q <query> argument layer truncates very long single-arg payloads around the 2.8 KB mark; the truncated query lands mid-projection, so ARG returns:
{
"error": {
"code": "BadRequest",
"details": [
{ "code": "InvalidQuery", "message": "Query is invalid." },
{ "code": "ParserFailure",
"message": "... characterPositionInLine=2757 token=<EOF> ..." }
]
}
}Symptom: Step.7 - Fleet Health Status failed with exit code 1 the moment the cmdlet was invoked. Step.7's separate, shorter "Detail" ARG query (rendered after Overview) was unaffected, which made the failure look like a HealthStatus-projection bug rather than an arg-length bug.
Fix: (1) the six // KQL comment lines are removed from the here-string and re-expressed as PowerShell # source comments above the $kql assignment - documentation for the source reader, no wire-side bytes. (2) The case() projection is compacted to one line (semantically identical). The wire query is now 2396 chars, back below the v0.7.72 baseline. A new inline IMPORTANT source comment above $kql flags the constraint so future contributors do not re-introduce it.
Verified end-to-end against the same live 20-cluster fleet: 20 cluster rows returned; HealthStatus distribution preserved from v0.7.73 (10 Healthy / 7 Critical / 2 Warning / 1 In progress); Step.7 HEALTHY_CLUSTERS output still writes 10 and the Fleet Health Overview table still renders the intended icons.
Operators reported the v0.7.73 Step.3 output was "very hard to follow and understand what to do, it needs much more detail and step by step for exactly what the operator needs to do to 'fix' the Apply Updates schedules, across the CRON jump for Step 5 and the Config YML file." v0.7.74 adds:
- Top-of-block "Fix-in-this-order checklist" when 2+ action sections are emitted, with N+1 ordered bullets that name the file to edit AND the silent-skip consequence of skipping that step (e.g.
Resolve-AzLocalCurrentUpdateRingreturns nothing for missing rings so Step.5 silently skips them;Test-AzLocalUpdateScheduleAllowednever opens the gate for uncovered UpdateWindows). The checklist surfaces the order the advisor uses internally (ring diff -> orphans -> unparseable crons -> cron coverage -> re-run) so the operator does not have to derive it from the section headings. **Why this matters.**paragraph in every section that names the specific runtime cmdlet that depends on the configuration being fixed (Resolve-AzLocalCurrentUpdateRing,Test-AzLocalUpdateScheduleAllowed) and the silent-skip failure mode the operator avoids by following the fix.- "How to fix - edit
<file>" subsection in the missing-rings section with a fullapply-updates-schedule.ymlskeleton snippet showing the existingschedule:block AND a placeholder row PER missing ring (TODO:markers onweeksInCycle,daysOfWeek,notes, annotated with the cluster count for the missing ring). The snippet carries anAzLocal.UpdateManagement v<version> advisor: add row(s) like these <<<header so it is unambiguous where the operator-edited content begins. - Ready-to-paste (uncommented) cron block in the cron-coverage section. Replaces the prior
# commentedform (which operators were copy-pasting verbatim including the#prefixes). The snippet is now a realon:(GitHub Actions) /schedules:(Azure DevOps) block, with one cron line per UpdateWindow plus a trailing yaml-#annotation showing the rings and cluster count served by each cron. - Platform-aware file labels. When
-Platformis pinned to a single platform, the text names the exact pipeline file (.github/workflows/Step.5_apply-updates.ymlvs.azuredevops/Step.5_apply-updates.yml) and the exact schedule file (.github/apply-updates-schedule.ymlvs.azuredevops/apply-updates-schedule.yml). - Two-choice fix tables for orphaned rings spell out both options (retag a cluster onto the ring via
Set-AzLocalClusterUpdateRingTag, OR remove the ring from the schedule file) so operators do not default to deletion when they actually wanted to bring a cluster onto the ring.
Test-AzLocalApplyUpdatesScheduleCoverage defaults to -Platform Both, which surfaced both the GitHub Actions schedule: block AND the Azure DevOps schedules: block in every Step.3 run regardless of which CI platform was running it. Both Step.3 yml files now pin -Platform GitHubActions (GH) / -Platform AzureDevOps (ADO) on both -View Audit and -View Recommend calls so the Step Summary contains exactly one platform-appropriate snippet.
All 14 Step.{1..7}.yml files (7 GitHub Actions + 7 Azure DevOps) bump GENERATED_AGAINST_MODULE_VERSION from '0.7.73' to '0.7.74'. Refresh existing copies via the marker-aware merge (preserves operator edits inside BEGIN-AZLOCAL-CUSTOMIZE:<region> / END-AZLOCAL-CUSTOMIZE:<region> marker pairs):
Update-AzLocalPipelineExample -Destination .\.github\workflows -Platform GitHub
Update-AzLocalPipelineExample -Destination .\.azure-pipelines -Platform AzureDevOpsAll v0.7.74 changes are backward compatible. Get-AzLocalFleetHealthOverview returns the same shape and the same normalised HealthStatus vocabulary it returned in v0.7.73 - only the underlying wire query is shorter so it no longer trips the truncation. The v0.7.74 Step.3 yml changes (adding -Platform GitHubActions / -Platform AzureDevOps) are recommended-but-not-required - the cmdlet still works against the v0.7.73 yml; you just continue to see the cross-platform noise until the yml is refreshed.
v0.7.73 is a bug-fix release on top of v0.7.72. Get-AzLocalFleetHealthOverview (the cmdlet that powers Step.7 - Fleet Health Status) was emitting the raw Azure Resource Graph properties.healthState enum values (Success / Failure / Warning / InProgress / NotKnown) on the HealthStatus column, but the cmdlet's own doc comment, the Step.7 pipeline filter Where-Object { $_.HealthStatus -eq 'Healthy' }, and the Step.7 Fleet Health Overview rendering switch ($o.HealthStatus) all expected the operator-friendly vocabulary Healthy / Critical / Warning / In progress / Unknown. Symptom: Step.7 reported Healthy Clusters = 0 against any fleet (live-verified against a 20-cluster fleet: 10 Success / 8 Failure / 2 Warning) and the overview table rendered the [Success] / [Failure] default-bracket fallback instead of the intended β
Healthy / β Critical icons. The KQL projection in the cmdlet now normalises in a case() clause so the HealthStatus column matches the documented contract; no pipeline-sample YAML change is required (the v0.7.72 pin already used the correct vocabulary). Pipeline pin bumps to '0.7.73'; refresh existing copies via Update-AzLocalPipelineExample.
Get-AzLocalFleetHealthOverview.HealthStatus is documented (and consumed) as one of Healthy / Critical / Warning / In progress / Unknown. The KQL projection joining microsoft.azurestackhci/clusters with the updateSummaries extensibility resource was, however, a one-line passthrough:
HealthStatus = iif(isempty(HealthState), 'Unknown', HealthState),The upstream ARG field properties.healthState emits the raw ARM enum values Success / Failure / Warning / InProgress / NotKnown, so Healthy / Critical / In progress literally never appeared on any row. The pipeline filter Where-Object { $_.HealthStatus -eq 'Healthy' } therefore matched zero rows and the HEALTHY_CLUSTERS GitHub Actions output was always 0; the rendering switch fell through to the default arm and rendered [Success] / [Failure] instead of β
Healthy / β Critical. The projection is now:
HealthStatus = case(
isempty(HealthState), 'Unknown',
HealthState =~ 'Success', 'Healthy',
HealthState =~ 'Failure', 'Critical',
HealthState =~ 'InProgress', 'In progress',
HealthState =~ 'NotKnown', 'Unknown',
HealthState
),Warning is passed through unchanged, and any unrecognised future state also passes through (rather than being silently bucketed as Unknown) so a platform addition stays visible. Verified end-to-end against a live 20-cluster fleet (10 Success / 8 Failure / 2 Warning): after the fix, Step.7 reports Healthy Clusters = 10, the Fleet Health Overview table renders the intended icons, and the Step.7 numbers are internally consistent with the Step.6 panel for the same fleet (Step.6 reports Up to Date = 10 and Critical Health Status: 12 passed / 8 failed; the 2 Warning clusters surface as β οΈ Warning in the overview without being counted as either Healthy or Critical). The cmdlet's doc comment is also corrected to describe the normalised vocabulary and to spell out the raw-to-normalised mapping for future-proof clarity.
All 14 Step.{1..7}.yml files (7 GitHub Actions + 7 Azure DevOps) bump GENERATED_AGAINST_MODULE_VERSION from '0.7.72' to '0.7.73'. The Step.0 authentication validation workflow does not pin a module version. No filter or switch changes required in the pipeline samples - the v0.7.72 pin already used the correct vocabulary; the v0.7.73 module simply emits values that match. Refresh existing copies via the marker-aware merge (preserves operator edits inside BEGIN-AZLOCAL-CUSTOMIZE:<region> / END-AZLOCAL-CUSTOMIZE:<region> marker pairs):
Update-AzLocalPipelineExample -Destination .\.github\workflows -Platform GitHub
Update-AzLocalPipelineExample -Destination .\.azure-pipelines -Platform AzureDevOpsAll v0.7.73 changes are backward compatible. The HealthStatus column type (string) and column position (third column, after ClusterName and ClusterPortalUrl) are unchanged - only the value set narrows from the raw ARG enum to the documented operator-friendly vocabulary. Operators who built custom downstream automation that explicitly filtered for the raw Success / Failure / InProgress / NotKnown strings against the v0.7.70 - v0.7.72 builds must update those filters to the new Healthy / Critical / In progress / Unknown vocabulary (or pull the raw value back via a separate ARG call on updateSummaries.properties.healthState if they specifically need the platform enum).
v0.7.72 is a pipeline-samples hotfix release on top of v0.7.71. Two issues observed when operators ran the v0.7.71 GitHub Actions samples in production: (1) the Step.1 / Step.2 / Step.5 GitHub Actions Summary panels rendered empty because the workflows used Write-Host "<markdown>" >> $env:GITHUB_STEP_SUMMARY - Write-Host writes to the PowerShell information/host stream (6), not stdout (1), so the >> redirect appended nothing to the summary file; (2) AZURE_TENANT_ID was stored as a GitHub Secret on the same rationale as the v0.7.71 AZURE_SUBSCRIPTION_ID treatment - a public ARM/AAD identifier, not a credential. Both are fixed. No PowerShell source files changed - this release is scoped to pipeline-sample YAMLs and Markdown docs. Refresh existing copies via Update-AzLocalPipelineExample.
Across these three workflows, 62 Write-Host "<markdown>" >> $env:GITHUB_STEP_SUMMARY lines were silent no-ops because Write-Host emits to PowerShell's information stream (stream 6), not stdout (stream 1). The file-redirect operator >> only attaches to stdout, so the GitHub Actions Summary file received an empty append on every line. The job log printed the markdown to the runner console (where operators do not normally look) but the GitHub Actions Summary panel - the canonical post-run report surface - rendered blank. All 62 lines now use the bare-string-to-stdout form ("<markdown>" >> $env:GITHUB_STEP_SUMMARY), so the totals/tables/actions-required block coded in v0.7.71 finally surface in the GitHub Actions UI. Affected:
- Step.1: cluster-inventory totals + UpdateRing distribution.
- Step.2: UpdateRing tag-management settings + dry-run notice.
- Step.5: update-application readiness/results matrix + actions-required block +
no-clusters-readyjob summary.
ADO pipelines were unaffected (they use the ##vso[task.uploadsummary] mechanism, not stdout redirection).
All 8 GitHub Actions Step.*.yml workflows (including the four commented-out legacy azure/login@v3 creds: JSON examples in Step.1 / Step.2 / Step.5) now read the Entra ID tenant id from vars.AZURE_TENANT_ID instead of secrets.AZURE_TENANT_ID. Rationale matches the v0.7.71 AZURE_SUBSCRIPTION_ID switch: the tenant id is a public ARM/AAD identifier - it is rendered in workflow telemetry on every azure/login@v3 run, present in the OIDC token issuer URL, and visible to anyone with read access to the App Registration - not a credential. It is consumed in exactly one place: the tenant-id: input to azure/login@v3, which exchanges the OIDC token for an Azure AD token in the named tenant. It is NOT used to scope Azure Resource Graph queries and is NOT interpolated into portal deep-link URLs. Treating it as a Variable also means it appears plaintext in workflow logs (matching its non-sensitive nature) and removes the need for an extra Secret rotation in tenants that already track it as a Variable. Steady-state setup is now one repo-level Secret (AZURE_CLIENT_ID) and two repo-level Variables (AZURE_TENANT_ID, AZURE_SUBSCRIPTION_ID); see updated docs in Automation-Pipeline-Examples/README.md. Azure DevOps pipelines authenticate via a service connection and need no change (the connection itself stores tenantId).
All 14 Step.{1..7}.yml files (7 GitHub Actions + 7 Azure DevOps) bump GENERATED_AGAINST_MODULE_VERSION from '0.7.71' to '0.7.72'. The Step.0 authentication validation workflow does not pin a module version. Refresh existing copies via the marker-aware merge (preserves operator edits inside BEGIN-AZLOCAL-CUSTOMIZE:<region> / END-AZLOCAL-CUSTOMIZE:<region> marker pairs):
Update-AzLocalPipelineExample -Destination .\.github\workflows -Platform GitHub
Update-AzLocalPipelineExample -Destination .\.azure-pipelines -Platform AzureDevOpsGitHub Actions operators must also create AZURE_TENANT_ID as a repository Variable (gh variable set AZURE_TENANT_ID --body <tenantId>) and delete the existing AZURE_TENANT_ID Secret to avoid drift between the two values.
All v0.7.72 changes are backward compatible. No cmdlet signatures or output shapes change. The Variable migration is a per-tenant setup change (GitHub Actions only).
v0.7.71 is a bug-fix + UX polish release on top of v0.7.70. Two render-correctness fixes (Step.3 markdown rendering as a code block, Step.4 critical-count under-reporting as 0), one new action-required section in Test-AzLocalApplyUpdatesScheduleCoverage -View Recommend for unparseable cron expressions, hyperlinked Cluster Name + collapsible Verbose Error in the Step.6 Update Run History table, and the AZURE_SUBSCRIPTION_ID Secret -> Variable migration for GitHub Actions samples. All changes are additive over v0.7.70; no behaviour change for callers that don't read the new columns.
Test-AzLocalApplyUpdatesScheduleCoverage -View Recommend -ExportPath *.md was wrapping the multi-section snippet inside an outer yaml ... fence. From v0.7.69 onwards the snippet itself carries markdown headings, action-required tables, AND its own inner yaml ... fence around just the cron block. CommonMark fence-matching is by triple-backtick count: the outer yaml opened fence A; the inner closed fence A; the outer ``` then opened a new fence that was never closed. Result: every markdown element the Step.3 pipeline appended after the recommend block (### Audit Detail - Cron coverage table, `### Reports Available` list, timestamp) was swallowed into the unclosed fence and rendered as a single grey monospace code block. The cmdlet now emits the snippet verbatim - the inner pair stays balanced, and downstream content renders as proper markdown again.
Step.4_assess-update-readiness.yml (GH + ADO) was reporting Critical health failures: 0 in the markdown summary while the JUnit XML showed 46 critical findings. Root cause: the pipeline was filtering $health | Where-Object { $_.Severity -eq 'Critical' }, but Test-AzLocalClusterHealth -PassThru returns per-cluster summary objects (ClusterName, HealthState, Passed, CriticalCount, WarningCount, Failures (nested array)), NOT flat finding rows. Severity lives on items inside the nested Failures array; the outer summary has CriticalCount instead. The pipeline now aggregates correctly via ($health | Measure-Object -Property CriticalCount -Sum).Sum and counts affected clusters via @($health | Where-Object { [int]$_.CriticalCount -gt 0 }).Count.
When one or more YAML cron lines used syntax the advisor cannot evaluate (DayOfMonth restrictions, step values, named day-of-week tokens), the Audit view surfaced them as UnparseableCron rows but the Recommend view ignored them - the operator had to cross-reference the Audit Detail table to find each one. -View Recommend now emits a new ## Action required - simplify unparseable cron expression(s) section between the schedule-fix sections (add these rings, prune orphaned rings) and the cron-coverage section. The table lists every offending cron with its source file:line and the parser's error message so the operator can rewrite the line directly from the Step Summary. Sequenced before cron coverage so parser-blind crons are fixed BEFORE the operator accepts the cron-coverage recommendation (which may otherwise over-suggest entries that duplicate what an already-correct-but-unparseable line is doing). When only one action overall applies the (N of M) numbering prefix is still dropped.
The ### Update Run History and Error Details markdown table in the Step.6 pipeline summary gains two UX upgrades:
- Cluster Name renders as a
[ClusterName](portalUrl)markdown link so the operator can jump straight to the Azure portal cluster blade. The per-rowClusterPortalUrlis now projected directly byGet-AzLocalUpdateRunFailures(new property in v0.7.71, see below) so the link is fleet-wide accurate (each row carries its own subscriptionId-bearing resource id). - Verbose Error Details renders inside an inline
<details><summary>Show error</summary><br><code>...</code></details>block so the full parser/orchestrator stack is preserved (no more 250-char truncation) but the table stays scannable - rows expand on click. HTML-special chars (<,>,&) are escaped to keep the renderer honest; newlines collapse to<br>so multi-line stack traces remain readable inside the collapsible block; pipes are escaped so the table row stays intact.
Both changes apply to GH Actions and Azure DevOps twins.
Every output row now carries a ClusterPortalUrl property (https://portal.azure.com/#@/resource{ClusterResourceId}) alongside the existing UpdateRunPortalUrl. Consumed by Step.6 to render Cluster Name as a deep link, and available to any other consumer that wants a cluster portal link without rebuilding the URL.
All 8 GitHub Actions Step.*.yml workflows now read the Azure subscription id from vars.AZURE_SUBSCRIPTION_ID instead of secrets.AZURE_SUBSCRIPTION_ID. The value is consumed ONLY by azure/login@v3 to set the default az account context for the small subset of cmdlets that REQUIRE a single-subscription default. It is NOT used to scope Azure Resource Graph queries (the module's Invoke-AzResourceGraphQuery helper omits --subscriptions when no -SubscriptionId is supplied, so ARG runs fleet-wide against every subscription the federated identity can read) and it is NOT interpolated into portal URLs (every cmdlet projects subscriptionId per-row from ARG and builds portal deep-links from that, so each link points to the cluster's actual subscription regardless of the workflow's default context). Treating the id as a Variable also means it appears plaintext in workflow logs, matching its public non-sensitive nature, and removes the need for an extra Secret rotation in tenants where the value already lives in a tenant-scope Variable. Azure DevOps pipelines were already authenticating via a service connection and need no change.
All 16 Step.*.yml files (8 GitHub Actions + 8 Azure DevOps) bump GENERATED_AGAINST_MODULE_VERSION from '0.7.70' to '0.7.71'. Refresh existing copies via the marker-aware merge (preserves operator edits inside BEGIN-AZLOCAL-CUSTOMIZE:<region> / END-AZLOCAL-CUSTOMIZE:<region> marker pairs):
Update-AzLocalPipelineExample -Destination .\.github\workflows -Platform GitHub
Update-AzLocalPipelineExample -Destination .\.azure-pipelines -Platform AzureDevOpsAll v0.7.71 changes are backward compatible. The new ClusterPortalUrl property on Get-AzLocalUpdateRunFailures is additive; the new ## Action required - simplify unparseable cron expression(s) section in -View Recommend only renders when unparseable cron lines exist; the GH Variable switch is a per-tenant setup change (see updated docs in Automation-Pipeline-Examples/README.md).
v0.7.70 is the Step.0 recurring authentication audit + Step.6 Update Run History section + Step.6 manifest-anchored rolling support window + Step.3 dual-section UX + Step.7 fleet-health hyperlinks release, plus two new exported cmdlets - Get-AzLocalFleetHealthOverview (ARG-first fleet-scale view of cluster health and update status) and Get-AzLocalLatestSolutionVersion (unauthenticated probe of the Microsoft Azure Local public solution-update catalog at aka.ms/AzureEdgeUpdates, used by Step.6 to anchor the SupportStatus column on the upstream release calendar). All changes are additive over v0.7.69; no behaviour change for callers that don't read the new columns.
Step.0_authentication-test.yml (GitHub Actions + Azure DevOps twins) is renamed from "Authentication Validation Test" to "Step.0 - Authentication Validation and Subscription Scope Report" and repositioned from a one-shot smoke test into a recurring audit intended to be re-run monthly (or after any RBAC change in the tenant). The pipeline now emits:
- A JUnit XML report (
auth-report.xml) with three testsuites - Authentication, Subscription Scope (one testcase per accessible subscription, plus a count testcase), and Resource Graph Reachability (cluster count visible to the pipeline identity). Rendered in the GitHub Checks UI viadorny/test-reporter@v3and in the Azure DevOps Tests tab viaPublishTestResults@2. - A markdown summary at the top of every run with
Count of subscriptions = Nand a per-subscription detail table (Name / SubscriptionId / TenantId / State). Written to$GITHUB_STEP_SUMMARYon GitHub Actions and uploaded via##vso[task.uploadsummary]on Azure DevOps. - A
auth-reportartifact (XML +subscriptions.json+subscriptions.csv) for ITSM / dashboard ingest.
Drift in the subscription scope visible to the pipeline identity is the earliest signal that downstream fleet reports are about to silently under- or over-count clusters, which is why the cadence is recurring rather than one-shot.
A new <testsuite name="Update Run History and Error Details"> testsuite in the Step.6 JUnit XML and a matching ### Update Run History and Error Details markdown table in the run summary surface up to 25 of the most recent unresolved Failed update runs across the fleet (last 30 days). Each row links to the Azure portal SingleInstanceHistoryDetails deep-link and includes Status / CurrentStep / Duration / LastUpdated / DeepestErrMsg for at-a-glance triage. Sourced from Get-AzLocalUpdateRunFailures -State Failed -OnlyUnresolved (ARG-first, fleet-scale).
Step.6 Fleet Update Status pipeline - new "Overall Fleet Update Status (Version Distribution)" section with manifest-anchored rolling support window
A new <testsuite name="Fleet Version Distribution"> testsuite is emitted as the first child of <testsuites> (before AzureLocalFleetUpdateStatus) and a matching ### Overall Fleet Update Status (Version Distribution) markdown section is prepended to the run summary (above ### Critical Health Status). One row per distinct CurrentVersion reported by Get-AzLocalClusterUpdateReadiness, sorted by cluster count descending, with columns: Version / YYMM / SupportStatus / Cluster Count / Percentage / Clusters.
SupportStatus uses a rolling 6-month YYMM window anchored on the Microsoft public catalog when reachable. The Step.6 step calls the new Get-AzLocalLatestSolutionVersion cmdlet (see below) to fetch the latest released solution version's YYMM from https://aka.ms/AzureEdgeUpdates and seeds a calendar-stepped 6-month window from it (e.g. latest YYMM 2604 -> 2604,2603,2602,2601,2512,2511). As soon as Microsoft publishes any release with a newer YYMM, the window slides forward and the oldest in-window YYMM falls out automatically - independent of what is installed in the fleet. The JUnit testsuite carries supportSource / latestReleasedYymm / latestReleasedVersion / manifestUrl / manifestFetchedAt properties; the markdown summary annotates which anchor source was used and (on success) prints the latest released YYMM + version.
When the manifest is unreachable from the runner the pipeline emits a non-fatal warning (::warning:: on GitHub Actions, ##vso[task.logissue type=warning] on Azure DevOps) and falls back to a fleet-observed top-6 YYMM heuristic so Step.6 still reports (supportSource='fleet-observed'). Supported = cluster's YYMM is inside the anchor window; Unsupported = older parseable YYMM; Unknown = empty / malformed CurrentVersion. The markdown section includes inline links to the Microsoft Azure Local lifecycle cadence and release information docs as the operator cross-check. No <failure> tags - the testsuite is informational and never breaks the build.
Unauthenticated probe of the Microsoft Azure Local public solution-update catalog at https://aka.ms/AzureEdgeUpdates. Returns the latest released solution version plus the rolling YYMM support window calendar-stepped from it (year-rollover honoured). Used by Step.6 to anchor the SupportStatus column on the upstream catalog instead of fleet-observed values, so older releases drop out of support automatically as soon as Microsoft publishes a newer YYMM.
Output PSCustomObject: LatestYYMM, LatestVersion, SupportedYYMMs[] (length = -SupportWindowMonths, default 6, configurable 1-24), AllReleases[], ManifestUrl, ManifestFetchedAt (UTC), SupportWindowMonths, Source = 'aka.ms/AzureEdgeUpdates'. Tolerant of both XML shapes the manifest exposes (ApplicableUpdate/UpdateInfo and PackageMetadata/ServicesUpdates/Update/UpdateInfo).
# Default - latest released YYMM + 6-month rolling window
Get-AzLocalLatestSolutionVersion | Format-List LatestYYMM, LatestVersion, SupportedYYMMs
# 12-month rolling window (e.g. for slower-moving regulated fleets)
Get-AzLocalLatestSolutionVersion -SupportWindowMonths 12
# Pin to a private mirror of the manifest (must be http(s))
Get-AzLocalLatestSolutionVersion -ManifestUrl 'https://internal.example.com/AzureEdgeUpdates.xml' -TimeoutSeconds 60ARG-first fleet health summary. One row per cluster, joining microsoft.azurestackhci/clusters with the cluster's updateSummaries extensibility resource via a single Azure Resource Graph batch read. 12 columns in fixed order: ClusterName, ClusterPortalUrl, HealthStatus, UpdateStatus, CurrentVersion, SbeVersion, AzureConnection, LastChecked, HealthResultsAgeDays (computed as datetime_diff('day', now(), LastChecked)), ResourceGroup, NodeCount, SubscriptionId. Sort: HealthResultsAgeDays desc, ClusterName asc so the most-stale clusters surface first.
# Whole-fleet rollup
Get-AzLocalFleetHealthOverview -SubscriptionId <subId> | Format-Table
# Filter to a ring (wildcard '***' = every cluster carrying ANY UpdateRing tag)
Get-AzLocalFleetHealthOverview -UpdateRingTag '***' -SubscriptionId <subId>
# Export to CSV for ITSM / dashboard ingest
Get-AzLocalFleetHealthOverview -SubscriptionId <subId> -ExportPath .\fleet-health-overview.csv -PassThruThe Test-AzLocalApplyUpdatesScheduleCoverage cmdlet's Audit rows now carry a Section discriminator that splits the audit into two conceptually distinct concerns:
Section |
Status values | Unit of work |
|---|---|---|
Schedule |
RingMissingFromSchedule, RingOrphanedInSchedule |
The ring (UpdateWindow / RequiredCronUTC deliberately empty - these are about ring mapping, not window mapping) |
Cron |
Covered, PartiallyCovered, Uncovered, MalformedTag, UnparseableCron |
The (ring, UpdateWindow) pair vs the Step.5 cron entries |
Audit rows now sort Schedule-first, then Cron. -View Recommend is now a multi-section markdown report: an "Action required - add these rings to apply-updates-schedule.yml" block when RingMissingFromSchedule rows exist (emitted FIRST), followed by an "Action required - cron coverage (paste into Step.5_apply-updates.yml)" block. When -SchedulePath is omitted, only the cron section is emitted (back-compat for v0.7.68 callers).
The Step.3_apply-updates-schedule-audit.yml pipeline YAMLs (GH + ADO) mirror this in their summary:
- Two JUnit
<testsuite>blocks (ScheduleCoverage+CronCoverage) so the Tests tab shows per-section pass/fail counts. - Two Audit Detail markdown tables (one per section) with conditional headings - empty sections still emit a placeholder row so the operator can see the section was evaluated.
- When
$hasIssues -and $reco, the-View Recommendcmdlet output is prepended above the detail tables so operators see the fix before scrolling. - The zero-row JUnit placeholder text is centralised via a new
Write-Suite -EmptyPlaceholderName 'No tagged clusters found - nothing to audit'helper (GH-vs-ADO parity preserved since v0.7.67).
Step.7 fleet-health pipeline - cluster portal hyperlinks + 3 new detailed columns + new Fleet Health Overview section
Get-AzLocalFleetHealthFailures (the v0.7.65 cmdlet) gains three deep-link / target-resource properties on Detail rows - TargetResourceName (the sub-resource that emitted the check failure, e.g. the NIC name), TargetResourceType (e.g. Microsoft.Compute/virtualMachines/networkInterfaces), and ClusterPortalUrl (https://portal.azure.com/#@/resource{ClusterResourceId} deep-link). Summary rows gain AffectedClusterPortalUrls aligned with AffectedClusters (same element count, same order, joined with '; '). The Summary now sorts Critical-first: Severity (Critical, then Warning, then everything else), then ClusterCount desc, then FailureCount desc - a Critical failure on 1 cluster ranks above a Warning affecting many clusters.
The Step.7_fleet-health-status.yml pipeline YAMLs (GH + ADO) consume these in three places:
- Summary and Detailed Results cluster cells render as
[ClusterName](portalUrl)markdown links. The Summary caps at the first 10 then renders... (+N more); the Detail tables link every cell. - Three new Detailed Results columns: Failure Remediation (auto-renders as
[link](url)when the value starts withhttps://, otherwise plain text), Target Resource Name, Target Resource Type. - New
### Fleet Health Overview (fleet rollup)section calls the newGet-AzLocalFleetHealthOverviewcmdlet and surfaces the per-cluster 9-column rollup alongside the failures table. The mappingOK/Critical/Warning/In progress/Failedis rendered with bracketed labels ([OK],[Critical], etc.) so the at-a-glance summary is greppable. New report artifacts:fleet-health-overview.csvandfleet-health-overview.json.
All 14 Step.*.yml files (7 GitHub Actions + 7 Azure DevOps) bump GENERATED_AGAINST_MODULE_VERSION from '0.7.69' to '0.7.70'. The recommended upgrade path is the marker-aware merge (preserves operator edits inside BEGIN-AZLOCAL-CUSTOMIZE:<region> / END-AZLOCAL-CUSTOMIZE:<region> marker pairs):
Update-AzLocalPipelineExample -Destination .\.github\workflows -Platform GitHub
Update-AzLocalPipelineExample -Destination .\.azure-pipelines -Platform AzureDevOpsAll v0.7.70 changes are backward compatible. The Section column defaults to Cron for downstream filters that don't know about the discriminator; new TargetResource* / ClusterPortalUrl / AffectedClusterPortalUrls properties are additive (existing pipeline summaries that read only the v0.7.69 schema keep working).
585 passed / 0 failed / 0 skipped (unit) + 16 passed / 0 failed / 0 skipped (Live-Integration, opt-in) - the unit suite stays hermetic and now includes a parallel, tag-gated Tests/Live-Integration.Tests.ps1 durable suite that asserts the three v0.7.70 ARG-first cmdlets (Get-AzLocalFleetHealthOverview, Get-AzLocalFleetHealthFailures, Get-AzLocalUpdateRunFailures) against a real Azure Local fleet. The Live suite is excluded by default and safe to leave in the repo: each Describe self-skips when az is not installed, not logged in, or pointed at a non-target subscription. Opt in with .\Tests\Invoke-Tests.ps1 -IncludeLive or -LiveOnly. Validated end-to-end against a 20-cluster fleet (49 unresolved health failures, 9 unresolved Failed update runs) in 1m26s.
v0.7.69 introduces the ring-aware apply-updates schedule - a single human-readable YAML file (apply-updates-schedule.yml, schemaVersion: 1) that controls which UpdateRing tag value the apply-updates pipeline targets on any given UTC date. The schedule is decoupled from cron entirely: Step.5's cron only controls how often the pipeline wakes up, the per-cluster UpdateWindow tag controls when during an eligible day the update actually runs, and this new file controls which ring is eligible TODAY. Five new cmdlets (one generator + one reader + one resolver + one previewer + one migrator) plus a two-way ring diff on Test-AzLocalApplyUpdatesScheduleCoverage round out the feature.
# 1. Generate a STRAWMAN from your live fleet's UpdateRing tag values.
# Every schedule row is emitted commented-out by design.
New-AzLocalApplyUpdatesScheduleConfig -OutputPath .\.github\apply-updates-schedule.yml
# 2. Open the file. REVIEW each strawman row, then UNCOMMENT the rows
# that match your change-control policy.
# 3. Preview the next ~cycle of firings BEFORE committing.
Get-AzLocalApplyUpdatesScheduleNextFirings `
-Schedule (Get-AzLocalApplyUpdatesScheduleConfig -Path .\.github\apply-updates-schedule.yml)
# 4. Refresh the Step.5 pipeline YAMLs so they pick up the v0.7.69 resolver wiring.
Update-AzLocalPipelineExample -Destination .\.github\workflows -Platform GitHub
# 5. Audit the fleet against the schedule (two-way ring diff).
Test-AzLocalApplyUpdatesScheduleCoverage `
-PipelineYamlPath .\.github\workflows\Step.5_apply-updates.yml `
-SchedulePath .\.github\apply-updates-schedule.yml `
-View AuditWithout an active (uncommented) row, the apply-updates pipeline hard-fails at the reader step with
'schedule:' list is empty - at least one row is required. This is the v0.7.69 safety gate, not a bug. It guarantees that a fresh strawman cannot accidentally start applying updates fleet-wide.
| Layer | File / Setting | Grain | Controls |
|---|---|---|---|
| 1 | Step.5_apply-updates.yml schedule: cron |
intra-day | HOW OFTEN the pipeline wakes up |
| 2 | apply-updates-schedule.yml (NEW in v0.7.69) |
day | WHICH UpdateRing is eligible on a given UTC date |
| 3 | Per-cluster UpdateWindow tag |
minute | WHEN during an eligible day the update is allowed to start |
The Step.5 cron firing on a non-matching day exits 0 with the explanatory Reason and does not start an update. The cron firing on a matching day passes the resolved UpdateRing value to Start-AzLocalClusterUpdate -ScopeByUpdateRingTag -UpdateRingValue <resolved>.
| Cmdlet | Role |
|---|---|
New-AzLocalApplyUpdatesScheduleConfig |
Generates a strawman schedule from the live fleet's UpdateRing tag values (or from -Rings for offline use). Every emitted schedule row is commented out by design. |
Get-AzLocalApplyUpdatesScheduleConfig |
Parses + validates a schedule file. Hard-fails with 'schedule:' list is empty when no active rows are present (the safety gate). |
Resolve-AzLocalCurrentUpdateRing |
Maps a UTC date to the matching UpdateRing(s) using cycle-week math anchored at cycleAnchorISOWeek / cycleAnchorYear. Union semantics: overlapping rows merge rings with ;. |
Get-AzLocalApplyUpdatesScheduleNextFirings |
Previews the next N days of resolved firings so operators can sanity-check the rotation BEFORE committing. |
Update-AzLocalApplyUpdatesScheduleConfig |
Idempotent schema-version migrator. Renames the original to <basename>.v<oldVersion>.old.yml on disk and writes the migrated content to the canonical path. The recipes table is intentionally empty in v0.7.69; the framework is in place for future schema bumps. |
The existing schedule-audit cmdlet gained an optional -SchedulePath parameter. When supplied, the audit emits two new status rows beyond the existing cron-vs-tags audit:
| Status | Meaning |
|---|---|
RingMissingFromSchedule |
A fleet ring (live UpdateRing tag value on a cluster) has no matching row in the schedule file - clusters in that ring will never be updated. |
RingOrphanedInSchedule |
A schedule ring (a rings: token in the schedule file) has no cluster carrying that tag value - the schedule row will never fire. |
Both rows are surfaced in the Step.3 summary table, the JUnit XML failure list, and the Markdown summary at the top of the Step.3 run page.
- Step.5 (apply-updates) now resolves the
UpdateRingfrom the schedule file on every scheduled firing. Manualworkflow_dispatch(GitHub Actions) / non-ScheduleBuild.Reason(Azure DevOps) runs still honour the operator-supplied-UpdateRingValueinput verbatim. A workflow-levelconcurrency:block (GitHub Actions only - Azure DevOps documents the equivalent Settings -> Triggers -> Limit concurrent runs option) prevents overlapping cron firings. - Step.3 (schedule audit) gained a
schedule_path/schedulePathinput (defaulted to the standard layout), adebugtoggle for self-service triage, and surfaces the newRingMissingFromSchedule/RingOrphanedInSchedulecounts.
apply-updates-schedule.example.ymlships as documentation only.Copy-AzLocalPipelineExampleandUpdate-AzLocalPipelineExampledo not copy or refresh it. Operators runNew-AzLocalApplyUpdatesScheduleConfigto generate a strawman starting from their live fleet.- The per-cluster
UpdateWindowtag, theUpdateSideloadedworkflow, and all existing cron schedules in the bundled pipeline YAMLs are unchanged.
- Schema
schemaVersion: 1is a hard break vs any pre-v0.7.69 experimental schedule format. There are no v0 -> v1 migration recipes shipped (the recipes table is intentionally empty inUpdate-AzLocalApplyUpdatesScheduleConfig); if you were running an experimental schedule from earlier development builds, regenerate viaNew-AzLocalApplyUpdatesScheduleConfig.
v0.7.68 is the ARG-first refactor and pipeline-rename release. Seven fleet-scale read cmdlets were collapsed onto a single Azure Resource Graph batch read each (removing the silent -ThrottleLimit no-op), all 16 bundled pipeline YAMLs were renamed with a Step.X_ ordering prefix so an alphabetic listing tells the story end-to-end, and a new Get-AzLocalUpdateRunFailures cmdlet exposes the deep-error breadcrumb path from update-run telemetry without per-cluster shell-outs. Backwards-compatible for already-deployed consumers: Read-AzLocalApplyUpdatesYamlCrons matches both new (Step.X_*.yml) and legacy filenames.
-
Seven cmdlets are now single-batch ARG reads with
-ThrottleLimitremoved.-ThrottleLimitwas a meaningless flag against the ARG batched read path; carrying it implied (incorrectly) that the cmdlet was doing a per-cluster fan-out and could be tuned for throttle. The new shape removes the parameter and collapses the call onto one ARG query, which (a) cuts subscription-level ARM call volume 5-10x on the common fleet-status pipelines and (b) lets the helper apply uniform 429/Retry-Afterretry logic in one place. Affected cmdlets:Get-AzLocalUpdateSummary,Get-AzLocalAvailableUpdates,Get-AzLocalClusterUpdateReadiness,Test-AzLocalClusterHealth,Get-AzLocalFleetProgress,Get-AzLocalFleetStatusData,New-AzLocalFleetStatusHtmlReport. All bundled pipeline YAMLs were updated to stop passing-ThrottleLimit. -
Invoke-AzResourceGraphQuerynow retries on HTTP 429 (throttle). Inspects theRetry-Afterresponse header when present and otherwise applies bounded exponential backoff (capped at the documented Azure Resource Graph throttling envelope). Large fleet sweeps no longer fall over at the throttling boundary; happy-path latency is unchanged. -
Invoke-AzResourceGraphQueryhardened againstaz.cmdCR/LF stdout truncation. A latent bug inaz.cmd(Windows runners) could chop the JSON payload at the first chunked-write boundary when stdout was piped through PowerShell, producing the N-row collapse where a 27-cluster fleet would surface as 24 rows. The helper now decodes the full UTF-8 payload explicitly; existing Pester tests pin the regression. -
Get-AzLocalFleetProgressno longer silently returns stale state when ARG returns zero rows. The previous code-path treated an empty ARG response as "no change" and returned the last cached state; consumers (including theStep.6_fleet-update-status.ymlJUnit emitter) therefore reported "everything green" on fleets that had been completely de-tagged or that hit a transient ARG error. The cmdlet now surfaces the empty-fleet condition explicitly so the operator can act on it.
- ARG-only deep-error extraction (9 levels deep into the
properties.state.progresstree ofmicrosoft.azurestackhci/clusters/updates/updateRuns) returns verbose error information at fleet scale without per-cluster Az SDK or REST shell-outs. Two views:-View Summary(one row per failed update run) and-View Detail(one row per leaf failure with the full breadcrumb to the failed step). Useful inStep.5_apply-updates.ymlpost-mortem reports and as a follow-up call afterGet-AzLocalFleetProgressreports failures.
- Marker-aware merge tool that refreshes a customer's copy of any bundled pipeline YAML to the version shipped with the current module while preserving operator edits inside
BEGIN-AZLOCAL-CUSTOMIZE:<region>/END-AZLOCAL-CUSTOMIZE:<region>marker pairs. Customer-side cron schedules inschedule-triggersand ITSM secret bindings initsm-secrets(Step.5 only) survive a module upgrade; everything outside the markers is replaced with the new bundled content. Supports-WhatIf,-DryRun, multi-file batch mode, and emits a per-file change manifest so the operator can review the merge before committing. This is the operator-friendly upgrade path thatCopy-AzLocalPipelineExample(clean overwrite) does not provide.
-
All 16 bundled pipeline YAMLs renamed with a
Step.X_ordering prefix so they sort by execution order in a customer's repo (Step.0_authentication-test.yml,Step.1_inventory-clusters.yml,Step.2_manage-updatering-tags.yml,Step.3_apply-updates-schedule-audit.yml,Step.4_assess-update-readiness.yml,Step.5_apply-updates.yml,Step.6_fleet-update-status.yml,Step.7_fleet-health-status.yml). Both platforms (GitHub Actions + Azure DevOps). The rename matches the documented operator runbook order so an alphabetic listing in the consumer's IDE / repo browser tells the story end-to-end. -
Backwards compatibility for already-deployed consumers:
Read-AzLocalApplyUpdatesYamlCrons(the schedule-audit scanner) glob list expanded to match both new (Step.5_apply-updates*.yml) and legacy (apply-updates*.yml) names. A customer who upgrades the module but has not yet re-runCopy-AzLocalPipelineExamplewill still see correct schedule-coverage audits. -
Each YAML's workflow display name now also carries the
Step.N -prefix. GitHub Actions sidebar sorts workflows alphabetically by the YAML's top-levelname:field, so the 8 workflows now readStep.0 - Authentication Validation and Subscription Scope Report...Step.7 - Fleet Health Statusand list in execution order. For Azure DevOps the leading title comment in each YAML carries the same prefix so the import wizard prefills the suggested pipeline definition name correctly. SeeAutomation-Pipeline-Examples/README.mdsection 1.1 for the full convention.
- Seven YAMLs now carry named
AZLOCAL-CUSTOMIZEmarker pairs (6 main pipelines xschedule-triggers, plusStep.5_apply-updates.ymlxitsm-secrets) wrapping the YAML regions operators commonly customise. Markers are pure YAML comments and have no runtime effect; they back the newUpdate-AzLocalPipelineExamplecmdlet (also new in v0.7.68) that performs a marker-aware merge so operator edits inside the marker pairs survive a module upgrade.Copy-AzLocalPipelineExampleremains the clean-overwrite tool for first-time install / forced refresh.
If you have copied any of the bundled workflows into your repo, the recommended upgrade path is the new marker-aware merge:
Update-AzLocalPipelineExample -Destination .\.github\workflows -Platform GitHub
Update-AzLocalPipelineExample -Destination .\.azure-pipelines -Platform AzureDevOps(Add -WhatIf first to preview the merge before writing files; add -Force to skip the per-file confirm prompt for non-interactive runs.)
This brings in the new file names and the Layer 1 marker scaffolding while preserving operator-customised cron schedules and ITSM secret bindings inside BEGIN-AZLOCAL-CUSTOMIZE / END-AZLOCAL-CUSTOMIZE marker pairs in your already-deployed YAMLs.
For a clean overwrite (first-time install or forced refresh that discards local edits), keep using Copy-AzLocalPipelineExample:
Copy-AzLocalPipelineExample -Destination .\.github\workflows -Platform GitHub -Update
Copy-AzLocalPipelineExample -Destination .\.azure-pipelines -Platform AzureDevOps -Update- New
docs/Cmdlet-Inventory-And-Design.mddocuments which cmdlets read vs write, which back-end they use (ARG vs Az SDK vs az CLI), and the design rules that keep read paths ARG-first (no-ThrottleLimit, no per-cluster Get-AzResource fan-out). Removes ambiguity about which back-end a new cmdlet should take.
v0.7.67 lands a Phase 3/4 CI/CD-parity refresh, a new maintainer-facing release-process document, and a six-item batch of in-depth-review fixes. No public surface change - every pre-existing pipeline and cmdlet keeps the same contract.
-
GitHub Actions schedule-audit pipeline now emits a passing testcase when the fleet has no tagged clusters. Previously the GH variant of
Step.3_apply-updates-schedule-audit.ymlwrote an empty<testsuite>whenever there were no(UpdateRing, UpdateWindow)rows to evaluate;dorny/test-reporterthen reported "no tests found", indistinguishable from a misconfigured reporter step. The pipeline now writes<testcase classname="ScheduleCoverage" name="No tagged clusters found - nothing to audit" />for the zero-row case, matching the long-standing Azure DevOps behaviour. The Tests tab reads cleanly as "passed (1/1)" regardless of whether the fleet has any tagged clusters yet, removing the daily false-alarm during onboarding and after fleet-wide tag clean-ups. -
Schedule-audit summary now surfaces the cron recommendation block at the TOP when coverage drift exists. Both
Step.3_apply-updates-schedule-audit.ymlfiles (GitHub Actions and Azure DevOps) previously emitted the recommended cron block at the bottom of the run summary, after the audit detail table. When the audit reported anyUncovered/PartiallyCovered/MalformedTag/UnparseableCronrows, operators had to scroll past the detail table to find the actionable fix. The pipelines now render an### Action required - paste these cron entries into Step.5_apply-updates.ymlsection immediately below the counts table when issues exist; when the fleet is fully covered, the recommendation block stays at the bottom as a reference. A new Pester guard asserts both YAMLs carry the conditional structure and the ordering. -
Pipeline README now includes an artifact-handoff data-flow diagram. Section 6 of
Automation-Pipeline-Examples/README.mdalready carried a phase-oriented flow diagram (Inventory -> Tag -> Rollout -> Steady-state); the new "Artifact handoffs at a glance" subsection makes the data flow explicit, showing which artifact each pipeline emits and which downstream pipeline consumes it (e.g.cluster-inventory.csv-> manual tagging edit + manage-tags pipeline;cluster-readiness.csv-> apply-updates;schedule-coverage-recommend.md-> manual paste intoStep.5_apply-updates.yml). Calls out the four artifacts operators most commonly trip on. -
Pipeline README section 11 (Security model) now documents per-job
permissions:blocks as an intentional security feature. Every shipped GitHub Actions workflow declares its own job-levelpermissions:block (id-token: write,contents: read,checks: writeonly where needed). The new bullet calls out that consumers should not lift these blocks to a top-levelpermissions:block when copying samples, explains how the per-job shape (a) limits token scope to exactly the job that needs the write and (b) lets you keepid-token: writeoff read-only summary jobs, and documents that the samples are compatible with the recommended Settings -> Actions -> General -> Workflow permissions = Read repository contents and packages permissions hardening. -
All Azure DevOps sample pipelines now share the same
azureSubscription:placeholder. Four files (Step.4_assess-update-readiness.yml,Step.7_fleet-health-status.yml,Step.6_fleet-update-status.yml,Step.3_apply-updates-schedule-audit.yml) previously used'YOUR-SERVICE-CONNECTION-NAME'while the other four ADO YAMLs used'AzureLocal-ServiceConnection'. All eight ADO pipelines now use'AzureLocal-ServiceConnection'plus a# Update with your service connection namecomment, matching the value documented in section 3.2 of the pipeline README. An operator who follows the auth setup verbatim no longer has to find-and-replace anything in the YAMLs.
- Documents the staged unlisted-release flow the module uses (publish -> immediately unlist -> validate against a test repo with
REQUIRED_MODULE_VERSIONexact-pinned to the candidate -> list after validation passes), the verification commands for each stage, and the Pester guardrails the release flow relies on. This puts the release rules in the repo rather than in tribal knowledge / chat history. Pipeline consumers do not need this doc; module maintainers do.
Six defence-in-depth fixes following the post-PR-#36 module review. None are user-visible behaviour changes for the happy path - every fix closes a class of silent-drift or silent-drop bug.
-
$script:ModuleVersionconstant inAzLocal.UpdateManagement.psm1was stuck at0.7.66while the manifest moved to0.7.67. The script-scope constant is the valueStart-AzLocalClusterUpdatewrites into its run-log header and the valueGet-AzLocalFleetStatusDatastamps into theModuleVersionfield of the exported fleet-state JSON, so every v0.7.67 run-log and every v0.7.67 fleet-state file was misreporting the producing module version - which is the exact field operators use to triage CI vs local-runner discrepancies. Bumped to0.7.67; a new Pester guardModule version constants are in sync between .psm1 and .psd1compares(Import-PowerShellDataFile).ModuleVersionagainstInModuleScope { $script:ModuleVersion }and fails any future drift at build time. -
cp1252 stderr-warning regression that v0.7.66 fixed in
Invoke-AzResourceGraphQueryhad three remaining ambush sites. The unsafeaz ... 2>&1 | ConvertFrom-Jsonpattern was still live inGet-AzLocalClusterInventoryresolving subscription display names viaaz account show,Invoke-AzLocalSideloadedAutoResetForClusterreading the cluster tags viaaz rest, andSet-AzLocalClusterTagsMergereading the tags-subresource viaaz rest. The twoaz restcallers now route through the existingInvoke-AzRestJson. Theaz account showcaller goes through a new private helperInvoke-AzCliJson, which applies the same stream-split-by-element-type pattern, auto-appends--only-show-errors, setsPYTHONIOENCODING=utf-8as defence-in-depth (restored infinally), and returns[PSCustomObject]@{ Ok; Data; Error }so callers no longer have to inspect$LASTEXITCODEmanually. Seven new Pester tests cover the helper. -
ConvertFrom-AzLocalCronExpressionnow accepts cron step syntax (*/N,<a>-<b>/N,<a>/N). The schedule advisor was falsely flagging crons such as*/15 * * * *(every 15 min),0 */6 * * 1(every 6 hr on Mondays), and0 9-17/2 * * 1-5(every 2 hr between 09:00 and 17:00 on weekdays) asUnparseableCron- even though both GitHub Actions and Azure DevOps schedule them correctly. The parser now expands*/Nover the field's full range,<a>-<b>/Nover the explicit[a,b]range, and<a>/Nover[a, max]. Step values must be positive integers; out-of-bounds bases still throw with the existing bounds messages. Four new Pester tests (3 positive +*/0negative); the pre-existing test asserting*/15was rejected has been flipped to assert the 672-fires-per-week expansion is correct. -
Reset-AzLocalSideloadedTagnow warns when a-ClusterResourceIdsentry does not match/clusters/<name>$. TheByResourceIdresolver previously dropped malformed entries (typos, trailing slash, wrong provider, truncated string) silently - the operator would see "no matching clusters" without any indication that an input had been excluded. The resolver now emitsWrite-Log -Level Warningnaming the exactResourceIdit skipped. This brings parity with theByName/ByTagresolvers, which already warned on lookup failure. New regression test. -
Tests/Invoke-Tests.ps1HTML footer no longer printsModule Version: System.Object[]. When nested modules are loaded (the default for this module - every Private/*.ps1 is a nested module),Get-Module AzLocal.UpdateManagementreturns one entry per loaded version, and.Versionon that array surfaces asObject[]. The HTML test report footer was therefore intermittently printingModule Version: System.Object[]. The test runner now selects the newest loaded version viaSort-Object Version -Descending | Select-Object -First 1before reading.Version. -
Import-AzLocalFleetStatenow caps input at 50 MB before reading. The helper previously calledGet-Content -Raw | ConvertFrom-Jsonwith no size check; pointing it at a multi-GB file (typo, mis-glob, malicious symlink) would have OOMed the runner. Real fleet-state files (Export-AzLocalFleetStateoutput) are tens of KB at most, so a 50 MB ceiling is ~3 orders of magnitude above any plausible legitimate input. The cap message names the actual file size in MB and explains what valid input looks like. Two new Pester tests (mocked-oversize trip + happy path).
485 passed / 0 failed / 0 skipped (was 471 at v0.7.66) - the +14 new tests cover the drift guard, Invoke-AzCliJson (7), cron */N (4), Reset-AzLocalSideloadedTag warning (1), and Import-AzLocalFleetState size guard (2).
If you have copied any of the bundled workflows into your repo, refresh them via:
Copy-AzLocalPipelineExample -Destination .\.github\workflows -Platform GitHub -Update
Copy-AzLocalPipelineExample -Destination .\.azure-pipelines -Platform AzureDevOps -Updatev0.7.66 is a CI/CD hotfix release. Two errors were observed on production runs of the fleet-health-status and apply-updates-schedule-audit pipelines on windows-latest GitHub Actions runners; both have a clean root cause and a minimal patch. No public surface change.
-
Fixed (critical) -
Get-AzLocalFleetHealthFailuresfailed JSON parsing when the Azure CLI emitted a cp1252 encoding warning to stderr. Onwindows-latestrunners (or any ADO Windows agent whose console code page iscp1252), the Azure CLI's underlying Python layer can surface this stderr line:WARNING: Unable to encode the output with cp1252 encoding. Unsupported characters are discarded.The shared
Invoke-AzResourceGraphQueryhelper was capturing& az graph query ... 2>&1as a single merged stream and feeding the entire thing toConvertFrom-Json. The WARNING line therefore got prepended to the JSON body and the cmdlet threwConversion from JSON failed with error: Unexpected character encountered while parsing value: W. Path '', line 0, position 0.This is the same class of bug that the v0.7.2 hardening ofInvoke-AzRestJsonalready handled - the fix never made it intoInvoke-AzResourceGraphQuerywhen that helper was split out. The actual fix in v0.7.66 is the post-capture stream split: the helper splits the merged2>&1stream by element type, surfacing stderr lines as[System.Management.Automation.ErrorRecord]objects (which2>&1boxes them as) and stdout lines as strings; only the string stream is fed toConvertFrom-Json. (The helper already passed--only-show-errorssince v0.7.2, but the cp1252 encode warning can still leak through on some character paths, hence the belt-and-braces split.) The helper also sets$env:PYTHONIOENCODING = 'utf-8'for the duration of the call as cosmetic defence-in-depth only - this is a structural no-op for stockaz.cmdbecauseaz.cmdlaunches Python with the-I(isolated) flag, which implies-Eand causes Python to ignore everyPYTHON*env var (confirmed in Azure/azure-cli#28497 and the v0.7.2 root-cause analysis). The assignment only takes effect on hosts that have manually patchedaz.cmdto remove-I. The previous value is restored in afinallyblock. Every fleet-health-status pipeline run on a Windows runner is now resilient to this stderr warning. -
Fixed (critical) -
Step.3_apply-updates-schedule-audit.yml(both GitHub Actions and Azure DevOps) shipped with a defaultpipeline_paththat only exists in this module's source repo. The shipped default wasAzLocal.UpdateManagement/Automation-Pipeline-Examples. In a consumer repo - whereStep.5_apply-updates.ymllives under.github/workflows/(GH) or.azure-pipelines/(ADO) - that path does not exist and every default-trigger run failed withPipelineYamlPath '...' does not exist on the runnerbefore the schedule advisor could write its JUnit XML, which then crasheddorny/test-reporterwithNo test report files were found. The defaults now match the standard consumer layout:.github/workflowson GitHub Actions,.azure-pipelineson Azure DevOps. When the resolved path is still missing on the runner, the audit step now lists which common pipeline folders do exist in the checked-out repo (.github/workflows,.azure-pipelines,pipelines, repo root) so the operator immediately knows what value to pass viaworkflow_dispatch/ queue-time override.
In addition to the two hotfixes above, v0.7.66 ships a focused UX refresh of the fleet-update-status summary, timestamped artifact downloads across every pipeline, and multi-value / wildcard UpdateRing inputs on every exposed pipeline parameter and cmdlet.
-
Status emojis in the Fleet Update Status summary.
Step.6_fleet-update-status.yml(GitHub Actions and Azure DevOps) now renders theCritical HealthandPrimary Statussummary tables with a green tick / red cross / refresh / yellow circle / info glyph - the same visual language operators already use to read JUnit dashboards. The legacy[ok] / [fail] / [ready] / [running] / [blocked] / [info]bracket markers are gone. Plain markdown, so it renders identically on the GH Actions step summary, the ADO pipeline run extension, and any markdown viewer (no JS, no images). -
Generation timestamp in the Fleet Update Status H2 heading. Both
Step.6_fleet-update-status.ymlfiles now render the H2 heading as## Fleet Update Status Summary _(generated YYYY-MM-DD HH:MM:SS UTC)_. Operators no longer have to cross-reference the pipeline run start time to know when the data was collected. -
Failed clusters appear before passing clusters in the JUnit per-cluster block. The per-cluster
<testcase>ordering inStep.6_fleet-update-status.yml(both platforms) is now bucketed: failed first (sorted alphabetically byClusterName), then passed (sorted alphabetically). Failing clusters lead the dorny/test-reporter view (GitHub) and the Tests tab (Azure DevOps). -
Every downloadable pipeline artifact now carries a UTC timestamp suffix. All
actions/upload-artifactsteps (GH) andPublishBuildArtifacts@1/PublishPipelineArtifact@1tasks (ADO) now declarename:/ArtifactName:of the formazlocal-<purpose>_yyyyMMdd_HHmmss. Each job computes the timestamp once in a dedicatedCompute Artifact Timestampstep. Two runs of the same pipeline on the same day now produce distinct zip downloads (azlocal-fleet-update-status-report_20260518_140000.zipvsazlocal-fleet-update-status-report_20260518_180000.zip) instead of clobbering each other in the operator's downloads folder. -
Pipeline
UpdateRinginputs accept multi-value lists and the literal***wildcard. Every pipeline that exposesupdate_ring:(GHworkflow_dispatch) orupdateRing:(ADOparameters:) now accepts:- A single ring (unchanged):
Wave1 - A semicolon-delimited list:
Prod;Ring2(whitespace around each ring is trimmed) - The wildcard
***(three stars, deliberate gesture) to match every cluster that has a non-emptyUpdateRingtag. Untagged clusters are excluded, preserving the existing opt-in gate. A single*, double**, or quadruple****are all REJECTED by the cmdlet's[ValidatePattern]so a one-character typo can no longer accidentally scope a fleet-wide write.
The ADO
Step.5_apply-updates.ymllost its closedvalues:enum (kepttype: stringso users still get a free-text editor in the ADO run dialog). - A single ring (unchanged):
-
[ValidatePattern]tightened on 15 cmdlets to match:^(\*\*\*|[A-Za-z0-9_-]{1,64}(;[A-Za-z0-9_-]{1,64})*)$. Each individual ring segment keeps the same 1-64 character[A-Za-z0-9_-]policy as v0.7.65; the changes are (a);-delimited lists are now accepted, and (b) only the exact three-character***token is accepted as a wildcard (single/double/quad stars are rejected). Hostile/malformed inputs (spaces, embedded quotes,<script>, leading/trailing;) are still rejected at the parameter binder before any Azure call is made. -
New private helper
ConvertTo-AzLocalUpdateRingKqlFiltercentralises the KQL clause construction for the three input forms above. Returns| where isnotempty(tags['UpdateRing'])for***(matches only tagged clusters), a| where tags['UpdateRing'] =~ 'single'clause for a single value, and a| where tags['UpdateRing'] in~ ('a','b')clause for a list. Embedded single quotes are doubled (KQL string-literal escape). All 12 ARG-query call sites in the public cmdlets now go through this helper. -
Pester regression coverage for every v0.7.66 feature: a
Describe 'v0.7.66 UpdateRing ValidatePattern accepts list & wildcard forms'that reflects on every cmdlet'sValidatePatternAttributeand asserts both the acceptance set (Wave1,Prod;Ring2,***, ...) and the rejection set including the easy-to-mistype*/**/****/*Wave1variants, plus the existing hostile inputs (Foo bar,abc'def,<script>, empty, leading/trailing;); aDescribe 'v0.7.66 ConvertTo-AzLocalUpdateRingKqlFilter helper'exercising every branch including the new***->isnotempty(...)path (both defaulttags['UpdateRing']andtostring(tags['UpdateRing'])accessors); aDescribe 'v0.7.66 Artifact download names carry a UTC timestamp suffix'that scans everyAutomation-Pipeline-Examples/**/*.ymland fails if any upload step is missing theazlocal-prefix or the<timestamp>token (plus four guards against the legacy non-stamped names regressing); aDescribe 'v0.7.66 Fleet Update Status summary uses status emojis and groups failures first'that asserts the U+2705 and U+274C glyphs are present and that the legacy[ok]/[fail]markers are gone, that the_(generated $generatedUtc)_heading is present, and that the$failedClusters/$passedClusters/$orderedClustersbucketing tokens all appear; and aDescribe 'v0.7.66 Pipeline update_ring inputs document multi-value and wildcard support'that asserts every input description mentions bothProd;Ring2and'***'.
If you have copied any of the bundled workflows into your repo, refresh them via:
Copy-AzLocalPipelineExample -Destination .\.github\workflows -Platform GitHub -Update
Copy-AzLocalPipelineExample -Destination .\.azure-pipelines -Platform AzureDevOps -Update-
New -
Get-AzLocalFleetHealthFailurescmdlet. Surfaces the in-flight 24-hour system health-check failures across every Azure Local cluster the caller can read, via a single Azure Resource Graph query. The 24-hour health checks run on Azure Local clusters independently of update activity - which means clusters that are already "up to date" can still surface Critical or Warning issues that need triage. This cmdlet (and the new pipeline below) are the dedicated entry point for that workflow. Two views:-View Detail(one row per (cluster, failing check)) and-View Summary(aggregated byFailureReason+Severity, ordered "most widespread first" so administrators can target the highest-impact fixes).-Severity Critical|Warning|Allfilters at the ARG side; Informational entries are always excluded.-UpdateRingTagnarrows to a specific wave. The function reuses the existingInvoke-AzResourceGraphQueryhelper for paginated CLI shell-out, so it inherits the same skip-token / error-scrubbing behaviour as every other fleet-wide query in the module. -
New - "Fleet Health Status" CI/CD pipeline samples in
Automation-Pipeline-Examples/github-actions/Step.7_fleet-health-status.ymlandAutomation-Pipeline-Examples/azure-devops/Step.7_fleet-health-status.yml. Daily 07:00 UTC schedule (offset fromfleet-update-statusat 06:00). Each run produces:- Pipeline run summary: pivoted by
FailureReason(top failures by cluster impact), with a per-cluster "Detailed Results" table mirroring the standard "24-Hour System Health Checks - Detailed Results" view. - JUnit XML: one
<testcase>per (cluster, failing health check), grouped underCritical Health Failures/Warning Health Failurestestsuites for two-level drill-down in dorny/test-reporter (GitHub) and PublishTestResults@2 (Azure DevOps). dorny is configuredlist-suites: failed+list-tests: failedso the Test Reporter block is collapsed by default and only expands the failures. - CSV exports:
fleet-health-detail.csv(per-cluster, per-failure) andfleet-health-summary.csv(one row per failure reason, ordered by cluster impact). Both are uploaded as build artifacts and are ready-made payloads for ITSM hand-off. Together withStep.6_fleet-update-status.yml, administrators now have two dedicated pipelines: one for "is each cluster up-to-date?" (Update Status) and one for "do clusters have actionable health issues even when up-to-date?" (Health Status).
- Pipeline run summary: pivoted by
-
New - Pester guardrail prevents pipeline-YAML version drift. A new Pester
Context 'Pipeline YAML version pin (v0.7.65)'test discovers every*.ymlfile underAutomation-Pipeline-Examples/that installs the module from PSGallery and asserts that theGENERATED_AGAINST_MODULE_VERSIONconstant in that YAML matches the module manifest version. Supports both the inline GitHub Actions shape and the two-line Azure DevOps shape. The test fails the build if any sample YAML is forgotten when the manifest is bumped. -
Fixed -
Set-AzLocalClusterUpdateRingTagnow uses the dedicatedMicrosoft.Resources/tags/defaultPATCH endpoint instead ofPATCH-ing the cluster resource. The previous code issuedPATCH https://management.azure.com/<clusterId>?api-version=2025-10-01with{ "tags": {...} }, which Azure RBAC routes through themicrosoft.azurestackhci/clusters/writeaction - i.e. full cluster Contributor. CI/CD service principals scoped to Tag Contributor (onlyMicrosoft.Resources/tags/*actions) therefore failed withAuthorizationFailed: action 'microsoft.azurestackhci/clusters/write'even though they should have been able to write tags. The function nowPATCHeshttps://management.azure.com/<clusterId>/providers/Microsoft.Resources/tags/default?api-version=2021-04-01with{ "operation": "Merge", "properties": { "tags": { "UpdateRing": "..." } } }, which Azure routes throughMicrosoft.Resources/tags/writeonly. The "Merge" operation preserves all other existing tags on the cluster without us having to re-send them. Action: if you scoped your tag-management SP to "Contributor" purely to work around the old behaviour, you can now safely scope it to the built-in Tag Contributor role on the cluster (or the resource group). -
Fixed - "Fleet Update Status" pipeline summary now reconciles with the JUnit pass/fail counts. Two related bugs were producing summary tables that did not add up to Total Clusters:
Up to Dateonly countedUpdateState -eq "UpToDate"and missed clusters reporting the (equally healthy)AppliedSuccessfullystate. Both states now count as "Up to Date".- The bucket counters were not mutually exclusive and there was no catch-all, so a fleet of 20 healthy + 8 failed clusters could render as
Up to Date: 0,Health Failures: 8, with the remaining 12 unaccounted for. Each cluster is now assigned to exactly one primary status using a priority cascade (Update Failed->Health Failure->SBE Prerequisite Blocked->Update In Progress->Ready for Update->Up to Date->Needs Investigation), so the rows always sum toTotal Clusters.
-
Changed - JUnit / step-summary ordering is now: Summary FIRST, Test Results SECOND in both
Step.6_fleet-update-status.ymlandStep.7_fleet-health-status.yml(GitHub + Azure DevOps). The markdown step summary is created before the JUnit XML is published, so the run-extensions / job-summary view leads with the operator-facing numbers rather than the raw test list. -
Changed - "Fleet Update Status" failure message now reads
UpdateState: ..., Health: ...(wasHealth: ..., UpdateState: ...) so the Update Status leads in both the JUnit<failure>message and the markdown summary. The Update Status is the primary signal for that pipeline. -
Documentation -
Set-AzLocalClusterUpdateRingTaghelp and the Automation-Pipeline-Examples RBAC guidance now both recommend the built-inTag Contributorrole for tag-management automation. -
New -
Test-AzLocalApplyUpdatesScheduleCoveragecmdlet + matching CI/CD pipeline samples. Read-only schedule-coverage advisor that compares the cron schedule(s) in yourStep.5_apply-updates.ymlpipeline to theUpdateWindowtag values present on your clusters and flags any(UpdateRing, UpdateWindow)pair that no cron will ever reach. Three views:-View Audit(one row per(Ring, Window)pair withCovered/Uncovered/PartiallyCovered/MalformedTag/UnparseableCronplus aRecommendationcolumn),-View Matrix(every distinct(Ring, Window)pair with the cron expression the advisor would generate for it),-View Recommend(ready-to-paste GH Actions and Azure DevOps cron blocks that cover every distinctUpdateWindowin the fleet). Per-segment cron generation handles multi-window tag values (Sat-Sun_02:00-06:00;Mon-Fri_22:00-04:00), day ranges (Fri-Mon), and configurable lead-time via-LeadTimeMinutes(default 5, 0-60). Pipeline samples ship atAutomation-Pipeline-Examples/github-actions/Step.3_apply-updates-schedule-audit.ymlandAutomation-Pipeline-Examples/azure-devops/Step.3_apply-updates-schedule-audit.yml, scheduled weekly Mondays at 05:00 UTC (before the daily fleet pipelines) so drift annotations land at the top of the Monday-morning operator queue. Each run produces a JUnit XML (one<testcase>per(Ring, Window)pair), three CSV/MD exports (schedule-coverage-audit.csv,schedule-coverage-matrix.csv,schedule-coverage-recommend.md), and a markdown step summary. Full end-to-end runbook (tag a ring -> see drift -> copy recommended cron -> verify) inAutomation-Pipeline-Examples/README.mdsection 8.3. -
Module version pin bumped to 0.7.65 in all 13 sample workflow YAMLs (11 pre-existing + the two new
Step.3_apply-updates-schedule-audit.ymlfiles). RunCopy-AzLocalPipelineExample -Updateafter upgrading to refresh the samples in your repo.
-
Fixed (critical) - pipeline-sample YAMLs (10 files across GitHub Actions and Azure DevOps) had accumulated cp1252 mojibake from previous emoji-edit round-trips. One of the multi-byte sequences in
Step.2_manage-updatering-tags.ymlcontained a YAML 1.2 forbidden C1 control character (U+008F), which caused GitHub Actions to reject the workflow at recent commits withInvalid workflow file/ generic YAML syntax error and the affected step never ran. The root cause was UTF-8 emoji bytes (e.g.F0 9F 93 84=[document]) being misread as cp1252 by a previous editor session, then re-saved as UTF-8 - producingC3 B0 C2 9F C2 93 C2 84, which containsC2 8F-> U+008F. YAML 1.2 disallows raw C1 control characters U+0080-U+009F (except U+0085 NEL) in scalar content. All non-ASCII bytes have been stripped from every sample workflow ([^\x09\x0A\x0D\x20-\x7E]), the affected Markdown step-summary sections restored with plain-ASCII status labels ([info],[ok],[running],[ready],[blocked],[fail]), and the YAMLs verified to round-trip cleanly through the GitHub Actions and Azure DevOps validators. No module code paths are affected; only the sample YAMLs. -
Security hardening (Medium) -
Connect-AzLocalServicePrincipaland six additional directaz restcallers now scrub raw CLI output throughConvertTo-ScrubbedCliOutputbefore logging or throwing. A strayrefresh_token/access_token/ cookie that theazCLI might emit on a failure can no longer reach the host logs verbatim. Sites:Set-AzLocalClusterTagsMerge(3 throw paths),Invoke-AzLocalSideloadedAutoResetForCluster,Invoke-AzLocalUpdateApply(verbose),Set-AzLocalClusterUpdateRingTag(failure path),Start-AzLocalClusterUpdate(subscription-set and validate paths), andConnect-AzLocalServicePrincipal. This closes the same Bearer-token leak class that was already handled insideInvoke-AzRestJson/Invoke-AzResourceGraphQuerybut missed by the seven sites that callaz rest/az account setdirectly. -
Documentation:
README.mdandITSM/README.mdnow carry explicit security notes about (a) theaz login --service-principal --password <secret>command-line exposure on the SP+secret authentication path (visible toWin32_Process.CommandLineand EDR for the duration of the call - prefer OIDC or Managed Identity), and (b) the unavoidable plaintext[string]residency of ITSM secrets in memory during ServiceNow OAuthclient_credentialsgrants (the ServiceNow REST surface requires plaintext POST bodies, so[SecureString]round-tripping is impossible at this layer; mitigations baked in: scrubbing, URI redaction, finally-block temp-file cleanup). -
Fixed (Low) -
Invoke-AzLocalUpdateApply$result -match "202"was running against thestring[]returned byaz restwhich is array-filter semantics, not regex-match. The comparison is now done against($result | Out-String).Trim()against the single regex'202|Accepted';Write-Verbosepath also scrubbed. -
Fixed (Low) -
Invoke-AzLocalItsmHttpthrowon non-retryable HTTP failure now uses$redactedUriinstead of$Uri. The redaction ((client_secret|access_token|password)=[^&]+->$1=***) was already applied to theWrite-Verboselog line; thethrowmessage bypassed it. -
Fixed (Low) - two Pester tests (
ScheduleBlockedandSideloadedBlockedJUnit XML coverage) wrote to fixed temp filenames that would collide if the test suite is run in parallel or back-to-back. Filenames now append a per-invocation[Guid]::NewGuid(). -
Module version pin bumped to 0.7.64 in all 10 sample workflow YAMLs. Run
Copy-AzLocalPipelineExample -Updateafter upgrading to refresh the samples in your repo.
-
Fixed (critical) -
Start-AzLocalClusterUpdateStep 3b critical-health gate was being silently bypassed. The caller invokedTest-AzLocalClusterHealthwithout-PassThru; the function logs to the host stream and returns$nullwithout-PassThru, so the predicate$healthResults[0].CriticalCount -gt 0always evaluated false even when the function had just loggedBLOCKED (N critical). Apply would then write "No critical health issues found - cluster is eligible for update" and proceed to PATCH the update despite critical health failures. Two more call sites inGet-AzLocalUpdateRunshad the same omission. All three fixed. -
Fixed (critical) - Tag writes now use the ARM tags subresource (
Tag ContributorRBAC, notclusters/write).Set-AzLocalClusterTagsMerge- used byStart-AzLocalClusterUpdateto stampUpdateVersionInProgress, byReset-AzLocalSideloadedTag, and by the sideloaded auto-reset path - nowPATCHes.../providers/Microsoft.Resources/tags/default?api-version=2021-04-01instead of the full cluster resource. This narrows the required RBAC frommicrosoft.azurestackhci/clusters/writetoMicrosoft.Resources/tags/write(built-in Tag Contributor). Up to 2 PATCHes per call: oneoperation=Mergefor keys being set, oneoperation=Deletefor keys whose input value is$null. Idempotent: skips keys whose value already matches. -
Fixed (critical) - JUnit XML
Statusmapping no longer reports skipped clusters as passed.Export-ResultsToJUnitXmlpreviously renderedNotReady,NotConnected,NoUpdatesAvailable, andNoReadyUpdatesas<system-out>(passed indorny/test-reporter), producing misleading all green CI summaries when apply had actually skipped clusters. They now render as<skipped>.UpdateNotFoundnow renders as<error type="UpdateNotFound">instead of<system-out>. -
Changed -
apply-updatespipeline samples now consume the readiness CSV. BothAutomation-Pipeline-Examples/github-actions/Step.5_apply-updates.ymlandAutomation-Pipeline-Examples/azure-devops/Step.5_apply-updates.ymlnow download thereadiness-reportartifact from thecheck-readinessjob, filter rows whereReadyForUpdate=True, and pass-ClusterResourceIds @(...)toStart-AzLocalClusterUpdateinstead of re-discovering clusters byUpdateRingtag. This guarantees the readiness gate's decision is enforced rather than advisory: a cluster flagged Blocked in readiness will not be touched by apply even if its tag still matches the ring. Apply still re-validates each cluster (Step 1b connectivity, Step 3b health, Step 3c schedule, Step 3b1 sideloaded) as defence in depth. -
Changed - Readiness output gains a
ClusterResourceIdcolumn.Get-AzLocalClusterUpdateReadinessemits the full ARM resource ID on every row (includingNotFound/Errorrows) so the apply step can pass it straight toStart-AzLocalClusterUpdate -ClusterResourceIdswithout a second Resource Graph query. The CSV column is additive - existing consumers that parse by header name keep working. -
Module version pin bumped to 0.7.62 in all 10 sample workflow YAMLs. Run
Copy-AzLocalPipelineExample -Updateafter upgrading.
What a successful Step.5_apply-updates.yml run looks like (dry-run against the Prod ring on a 9-cluster sandbox - 4 ready, 5 skipped, zero failures):
-
Fixed (critical) -
Step.6_fleet-update-status.ymlpipeline samples failed withParserErrorunder PowerShell 7. Inside the PS double-quoted here-string that builds the Markdown step summary, Markdown code-span backticks before file names like`update-summaries.csv`were interpreted by the PS 7 parser as the new`u{xxxx}Unicode escape (which expects{immediately after`u). PS 5.1 had silently consumed the backtick; PS 7 hard-errors. Latent corruption also affected`readiness-status.csv`(`r-> CR),`available-updates.csv`(`a-> BEL), and`cluster-inventory.csv`(`c-> backtick dropped) - producing rendered job summaries with stray control characters and missing code-span formatting even on PS 5.1. All Markdown code-span backticks in the affected here-strings have been doubled; under both PS 5.1 and PS 7 two consecutive backticks in a double-quoted string produce exactly one literal backtick. No module code paths affected; only the pipeline-sample YAMLs. -
Module version pin bumped to 0.7.63 in all 10 sample workflow YAMLs.
-
Readiness now blocks
ReadyForUpdatewhen the cluster is not reachable or has Critical-severity health checks. BothGet-AzLocalClusterUpdateReadinessandGet-AzLocalFleetStatusData(driving the fleet HTML report) downgradeReadyForUpdatetoFalseand record the reason on a newBlockingReasonsCSV column whenever either of these is true:- Connectivity gate -
ClusterStateis not'ConnectedRecently'(e.g.NotConnectedRecently,Disconnected). ARM cannot reliably push an update to a cluster that has missed its heartbeat. - Critical health gate -
HealthCheckFailurescontains at least one[Critical]severity entry. These must be cleared before any solution upgrade is started.
Values on
BlockingReasonsare semicolon-joined (e.g.CriticalHealthCheck,CriticalHealthCheck; NotConnectedRecently). The per-cluster console output now showsBlocked (<reasons>)in red, and the summary footer reportsBlocked by Readiness Gate: Nalongside the existingBlocked by SBE Prereqcount. The CSV column is additive - existing consumers that parse by header name keep working. - Connectivity gate -
-
Start-AzLocalClusterUpdatedefence-in-depth: newStep 1bconnectivity gate. Immediately after cluster lookup, any cluster whoseproperties.statusis not'ConnectedRecently'is skipped before any update is attempted. A row is recorded inUpdate_Skipped.csvwith the message "Update not started - cluster status is '' (ARM cannot reach the cluster)", and the in-process$resultslist gets aStatus='NotConnected'row. Complements the existingStep 3bcritical-health gate. No new flags - the gate is always on. -
Fixed - JUnit XML
Statuscolumn fromGet-AzLocalClusterUpdateReadinesswas alwaysSkippedfor Ready clusters (long-standing bool/string comparison bug$_.ReadyForUpdate -eq 'Yes'against[bool]$true). JUnitStatusnow correctly emitsReady,Blocked,Failed, orSkipped. The CSVReadyForUpdatecolumn (a real[bool]) was unaffected; only JUnit XML readability in CI/CD test summaries was wrong. -
Module version pin bumped to 0.7.61 in all 10 sample workflow YAMLs. Run
Copy-AzLocalPipelineExample -Updateafter upgrading.
-
GitHub Actions sample workflows refreshed for Node 24. All five workflow YAMLs under
Automation-Pipeline-Examples/github-actions/now pin Node 24-compatible major versions of the third-party actions they use. This removes the "Node.js 20 actions are deprecated" warning banner that GitHub Actions started surfacing ahead of the Sept 16 2026 Node.js 20 hard-removal. No input/output surface changes for any of the bumped actions - refreshed pipelines work without any other edits:actions/checkout@v4->@v5(Node 24 default since v5.0.0)actions/upload-artifact@v4->@v6(v6 = Node 24 default; v5 still defaulted to Node 20)azure/login@v2->@v3(v3.0.0 = Node 24)dorny/test-reporter@v1->@v3(v3 = Node 24)
Already-deployed pipelines continue working on the older majors until Sept 16 2026. To pull the refreshed YAMLs into your existing
.github\workflows\folder after upgrading the module, runCopy-AzLocalPipelineExample -Update -Platform GitHub -Destination .\.github\workflows. -
Fixed -
dorny/test-reporter403 onStep.5_apply-updates.yml(GitHub Actions sample). Both jobs (check-readinessandapply-updates) only grantedid-token: write+contents: readin theirpermissions:block, missing thechecks: writepermission thatdorny/test-reporterneeds to create the Check Run that publishes JUnit results. Symptom: the test-reporter step failed withHttpError: Resource not accessible by integration(HTTP 403) on everyworkflow_dispatchrun, becauseworkflow_dispatchcontexts have no PR check-run context to write back to by default. The run itself was unaffected - readiness assessment and the subsequent apply still completed - this only restored the Check Run summary surface so the JUnit XML actually shows up in the GitHub UI. Sibling workflows (Step.4_assess-update-readiness.yml,Step.6_fleet-update-status.yml) already declaredchecks: writefrom v0.7.50; onlyStep.5_apply-updates.ymlwas missing it. -
Module version pin bumped to 0.7.60 in all 10 sample workflow YAMLs.
GENERATED_AGAINST_MODULE_VERSIONis the value the in-pipeline install step compares the actually-installed module version against, then warns if your local copy of the YAML is stale (see Automation-Pipeline-Examples/README.md section 5.3). All five GitHub Actions YAMLs and all five Azure DevOps YAMLs have been bumped in lock-step.
Copy-AzLocalItsmSample(new convenience function): copies the bundled ITSM connector sample (azurelocal-itsm.yml+templates/incident-body.md) out of the module install location into a user-chosen destination. Default-Destinationis.\.itsm- the exact relative path that bothStep.5_apply-updates.ymlworkflows defaultitsm_config_path/itsmConfigPathto (resolved relative to the repo root at job runtime). Same overwrite semantics asCopy-AzLocalPipelineExample: refuses by default,-Updateopts into per-fileShouldContinueprompts (withYes-to-All/No-to-Allthat survive across iterations),-Confirm:$falsebypasses the prompts for unattended use,-WhatIfpreviews without changing anything,-PassThrureturns the destination[DirectoryInfo]. Closes the gap where runningCopy-AzLocalPipelineExample -Platform GitHub(or-Platform AzureDevOps) deliberately did not bring the.itsm/sample along - the two functions now compose for a one-paragraph setup. The ITSM YAML itself is CI-platform-agnostic; both GitHub Actions and Azure DevOps consume it identically, only the secret source differs (repo / environment secrets vs. variable group).Copy-AzLocalPipelineExample- simpler, safer copy semantics. First real-world run of the v0.7.4 surface exposed two issues with the GitHub Actions path: (1)-Platform GitHub -Flattenleft an intermediategithub-actions\subfolder, so workflows landed at.github\workflows\github-actions\*.ymlwhere the GitHub Actions runner cannot see them (the runner only scans.github/workflows/*.yml, non-recursively); (2) the-Forceflag's directory-level pre-flight refused to copy whenever.github\workflows\contained any unrelated user workflow, making-Forceeffectively mandatory and meaningless. Both flags have been removed and the function has been reshaped around the user's actual intent:-Platform GitHubnow copies ONLY the*.ymlworkflow files from the sourcegithub-actions/folder directly into-Destination(flat - no wrapper folder, no README, no.itsm/). Canonical call:Copy-AzLocalPipelineExample -Destination .\.github\workflows -Platform GitHub.-Platform AzureDevOpsbehaves the same way against the sourceazure-devops/folder.-Platform All(the default) is unchanged - copies the full source tree under a.\Automation-Pipeline-Examples\child folder for browsing.- Controlled refresh via
-Update(new in v0.7.50): the function still refuses to overwrite by default and lists every conflict in the error message - but the error now points at the-Updateswitch instead of asking the user toRemove-Itemfirst. With-Updatethe function emits a per-fileShouldContinueprompt (Y/A/N/L/S/?) before each overwrite. Pair with-Confirm:$falseto suppress the prompts entirely (the documented automation / CI bypass), or use-WhatIfto preview without changing anything. Pipeline files are expected to live under git source control sogit diffis the second safety net afterShouldContinue. There is deliberately no-Force:-Updateis the narrower, more explicit replacement. - Pre-existing unrelated files in
-Destination(e.g. your repo's ownbuild.yml,codeql.yml) are now left untouched; the function only writes the files it is bringing over from the source tree. - Next-steps output is now platform-aware and detects when
-Destinationis already.github\workflows\("you're done, commit and push") vs. somewhere else ("move the YAMLs into.github\workflows\"). For both platform-specific values the output points atStep.0_authentication-test.ymlas the recommended first run (see sections 5.1 and 5.2 of the Automation-Pipeline-Examples README) so the user validates the auth chain before wiring the other five workflows. - Note: this is not marked as a breaking change because the v0.7.4 surface had not been adopted by any consumer at the time of removal (the feature shipped on 2026-05-13 and was found broken on first real-world use).
- Hotfix - parallel fleet reads broken by the v0.7.3 NestedModules refactor: v0.7.41 fixes two related regressions that surfaced on the PSGallery-installed v0.7.4 build whenever
-ThrottleLimit > 1was used.- Bug 1: every fleet read dispatched through
Invoke-FleetJobsInParallel(Get-AzLocalUpdateRuns,Get-AzLocalUpdateSummary,Get-AzLocalClusterUpdateReadiness,Get-AzLocalAvailableUpdates,Get-AzLocalFleetProgress,Invoke-AzLocalFleetOperation,Test-AzLocalClusterHealth, andStart-AzLocalClusterUpdate's parallel path) returnedState = Errorfor every cluster with the message "Cannot use '&' to invoke in the context of module 'Invoke-FleetJobsInParallel' because it is not imported." Inline (-ThrottleLimit 1) execution was unaffected. Root cause: the v0.7.3 refactor that split the monolithic.psm1intoNestedModuleschanged the meaning of$PSCommandPathinsideInvoke-FleetJobsInParallel.ps1- it now resolves to that helper's own.ps1, not to the rootAzLocal.UpdateManagement.psd1. Each per-batchStart-Jobscriptblock then imported only that single.ps1in the child runspace as a transient module, so subsequent& $module { Get-AzLocalClusterUpdateRuns ... }calls resolved against a session state that contained none of the private helpers. - Bug 2:
New-AzLocalFleetStatusHtmlReport -ThrottleLimit > 1(viaGet-AzLocalFleetStatusData) threw at start-up: "Parallel collection requires module path '...\Public\AzLocal.UpdateManagement.psm1' to be reachable by background jobs, but it does not exist." Same class of regression but a separate code path:Get-AzLocalFleetStatusDatacomputed its own module path usingJoin-Path $PSScriptRoot 'AzLocal.UpdateManagement.psm1', which under the post-v0.7.3 layout resolves to thePublic/subfolder - one level too deep.New-AzLocalFleetStatusHtmlReport's footer manifest-fallback had the same flaw.
- Bug 1: every fleet read dispatched through
- Centralised module-root resolution: introduced a new private helper
Get-AzLocalModuleRootManifestPathused by all three sites. It prefers the loaded module's.Path(.psd1over.psm1) and falls back to walking up from the caller's$PSCommandPath, so it returns the correct root manifest path from any file underPublic/orPrivate/. Future helpers won't reintroduce the same "$PSScriptRootis module root" assumption. - Regression coverage: existing parallelisation tests only exercised the inline
-ThrottleLimit 1fast-path, which reuses the parent runspace and so could not reproduce either bug. v0.7.41 adds Pester tests that (a) assertInvoke-FleetJobsInParallelpasses the root manifest path (.psd1/.psm1) as the trailingModulePathargument and not the helper's own.ps1, and (b) directly exerciseGet-AzLocalModuleRootManifestPathwith synthetic caller paths underPublic/andPrivate/. Full suite: 354/354 green.
- ITSM Connector - Phase 1 (ServiceNow): new opt-in capability for opening ServiceNow incidents directly from update / fleet pipeline output. Three new exported functions -
New-AzLocalIncident,Get-AzLocalItsmConfig,Test-AzLocalItsmConnection- plus supporting private helpers (Resolve-AzLocalItsmSecret,Get-AzLocalItsmDedupeKey,Get-AzLocalItsmTriggerDecision,Format-AzLocalIncidentBody,Invoke-AzLocalItsmHttp,Invoke-AzLocalServiceNowAdapter).New-AzLocalIncidentreads a JUnit results artifact (and an optional readiness CSV), applies the trigger matrix from the user'sazurelocal-itsm.ymlconfig, computes a deterministic SHA256 dedupe key per{ClusterResourceId, UpdateName, TriggerCategory}tuple, queries ServiceNow for an existing open ticket (u_azlocal_dedupe_key,stateIN1,2,3), and either creates a new incident or returns the existing ticket - so re-running the same pipeline is idempotent. Auth is OAuth 2.0client_credentialsonly in Phase 1; secrets resolve from Azure Key Vault (kv://<vault>/<secret>), environment variables (env://NAME), or explicit literals (literal://...with-AllowLiteral). HTTP path is TLS 1.2+, default 30s timeout, exponential backoff on 429/5xx,Retry-Afterhonoured. The CSV export from-ExportPathis sanitised byConvertTo-SafeCsvCollection, neutralising the same formula-injection class already handled elsewhere in the module.Test-AzLocalItsmConnectionvalidates the config, resolves secrets, performs the OAuth token grant, and probes a one-row read against/api/now/table/incident(matching the least-privilege scope used by ticket creation). Full documentation underITSM/: README, ITSM-Connector-Plan.md, ITSM-Config-Reference.md. A ready-to-copy sample config plus the Mustache ticket-body template live underAutomation-Pipeline-Examples/.itsm/. 33 new ITSM Pester tests; full suite green at 337/337. - Phase 2 and Phase 3 are not in this release. Phase 2 (
Sync-AzLocalIncident- close-out / work-note sweep when the underlying cluster recovers) and Phase 3 (Microsoft Teams + Slack mirror adapters) are designed inITSM/ITSM-Connector-Plan.mdand tracked for a later release. Thelifecycleandnotificationsconfig sections are parsed and stored in Phase 1 but not yet acted on. A small set of Phase 1.5 follow-ups is also tracked: cmdbCi token expansion, custom-field presence check, rate-limit-headroom probe, in-module token caching,Invoke-AzLocalItsmHttp -AllowedThumbprintscert pinning, andraiseAfterConsecutiveOccurrencesenforcement (requires the run-history store). - Code hygiene: removed the unused username/password OAuth grant path from the ServiceNow adapter (Phase 1 is
client_credentialsonly) - silences the PSScriptAnalyzerPSAvoidUsingUsernameAndPasswordParamsError andPSAvoidUsingPlainTextForPasswordWarning. Tightened the ITSM config validator sosecrets.source: mixednow requiressecrets.keyvaultName, matching the documented behaviour. Cleaned up legacy non-ASCII characters inPrivate/Format-AzLocalUpdateRun.ps1andPublish-Module.ps1divider comments. FlattenedITSM/Docs/intoITSM/(removed the stale duplicateITSM/Docs/ITSM-Connector-Plan.md). Copy-AzLocalPipelineExample(convenience): new exported function that copies the bundledAutomation-Pipeline-Examples/folder out of the module install location into a destination folder you control (default: current directory). Supports-Platform GitHub | AzureDevOps | All,-Flatten(drop contents directly into the destination),-Force(overwrite),-PassThru,-WhatIfand-Confirm. After copying, prints a short "next steps" summary pointing at the README and the platform-specific destination paths so you don't have to hunt through$module.ModuleBaseto find the YAML samples.
- Module renamed from
AzStackHci.ManageUpdatestoAzLocal.UpdateManagement. The module GUID is preserved across the rename so PowerShell tooling sees this as the same module identity. All previously-publishedAzStackHci.ManageUpdatesversions have been unlisted from PSGallery; a transitional v0.7.3 stub is published once to point legacy automation at the new name. Migration:Uninstall-Module AzStackHci.ManageUpdates -AllVersions; Install-Module AzLocal.UpdateManagement. Default log folder default also moved fromC:\ProgramData\AzStackHci.ManageUpdates\toC:\ProgramData\AzLocal.UpdateManagement\. - Internal refactor: the monolithic 11,679-line
.psm1has been split into Public/Private dot-sourced files matching the layout ofAzLocal.DeploymentAutomation. 20 exported functions live underPublic/, 40 internal helpers underPrivate/. The manifest'sNestedModuleslist enumerates every file. No functional change; the full Pester suite (299 tests) remains green.
- Bug fix - fleet read paths under
-ThrottleLimit > 1:Get-AzLocalUpdateRuns,Get-AzLocalUpdateSummary, andGet-AzLocalClusterUpdateReadinesspreviously failed for every cluster when invoked with-ThrottleLimitgreater than 1, reportingThe term 'Get-AzLocalClusterUpdateRuns' is not recognized...(or the equivalent for other private helpers). The per-cluster scriptblock dispatched viaStart-Jobcalled module-private helpers (Invoke-AzRestJson,Get-AzLocalClusterUpdateRuns,Format-AzLocalUpdateRun,Get-LatestUpdateByYYMM,ConvertTo-AzLocalAdditionalProperties,Get-HealthCheckFailureSummary,Get-TagValue) by name; because those helpers are filtered out byExport-ModuleMember, afterImport-Modulein the child runspace they were not visible at script command-resolution scope. Inline (-ThrottleLimit 1) execution was unaffected. Each affected scriptblock now captures a reference to the loaded module and either invokes the helper via& $module { ... }or rebinds the helper's bound scriptblock into the local function scope, so calls execute against the module's own session state and resolve all transitive private references. Reported against a 9-cluster Prod fleet. - Bug fix - cp1252 encoding warnings leaking into JSON parsing: On Windows hosts where the console code page is
cp1252(the English-US default), the Azure CLI (az rest,az graph query) emittedWARNING: Unable to encode the output with cp1252 encoding. Unsupported characters are discarded.whenever ARM responses contained non-cp1252 characters (smart quotes, accented characters in cluster tags, localised health-check messages, etc.). Captured via2>&1, that warning was prepended to the JSON body and brokeConvertFrom-Json, which silently dropped update runs and available updates for affected clusters. Earlier attempts to fix this via$env:PYTHONIOENCODING = 'utf-8'are structurally ineffective:az.cmdlaunches Python with the-I(isolated) flag, which implies-Eand so causes Python to ignore allPYTHON*environment variables (includingPYTHONIOENCODINGandPYTHONUTF8) - confirmed in Azure/azure-cli#28497. The actual fix is to pass--only-show-errorsto everyaz restandaz graph queryinvocation (Azure CLI maintainer's recommended workaround per Azure/azure-cli#14426). This suppresses the encode warning at source so the captured stderr/stdout streams stay clean. Characters that fail to encode are still replaced silently inside the CLI, but for ARM cluster/update payloads (timestamps, GUIDs, status enums, resource IDs - all ASCII) this is a non-issue. Genuine errors (auth failures, 4xx/5xx ARM responses, invalid args) still surface normally.
- Sideloaded payload workflow (new): opt-in two-tag protocol (
UpdateSideloadedset by operator,UpdateVersionInProgressset by module) for staging out-of-band update payloads.Start-AzLocalClusterUpdateblocks withStatus = SideloadedBlockedwhile the payload is being staged.Get-AzLocalUpdateRunsauto-resets the tags once the matching run succeeds. New public functionReset-AzLocalSideloadedTagfor explicit /-Forcerecovery. See section 7a for the full flow. No new RBAC required - rides on the existingMicrosoft.Resources/tags/read|writepermissions. - EndTime column for update runs: new
EndTimecolumn onGet-AzLocalUpdateRunstable output, sourced fromproperties.progress.endTimeUtc(most accurate "work finished" timestamp), falling back toproperties.lastUpdatedTime. Blank forInProgressruns. Per-runDurationnow prefers ARM-reportedproperties.duration(ISO-8601 timespan) over the computedEndTime - StartTimedelta, so it is immune to clock skew. Fleet HTML report's "Recent Update Run History" gains anEnd Timecolumn; JUnit XML test bodies includeStart Time:/End Time:lines per testcase (the JUnittime=attribute is unchanged). - Idempotent tag merges:
Set-AzLocalClusterTagsMerge(private helper used by bothSet-AzLocalClusterUpdateRingTagand the sideloaded workflow) now skips the PATCH when the merge produces no actual change against the cluster's current tags. Quieter logs and one less ARM write per no-op. - Enterprise-readiness review fixes:
- Security:
Write-UpdateCsvLog(the diagnostic CSV path used during apply runs) now sanitises every field throughConvertTo-SafeCsvFieldbefore quote-escaping, closing the same Excel-formula-injection gap on the interimUpdate_Skipped.csv/Update_Started.csvlogs that was already covered for the final exported results. - Operational: parallel
Get-AzLocalFleetStatusDatajob dispatch now treatsStoppedandDisconnectedjob states as failures alongsideFailed. Previously these terminal states fell through intoReceive-Joband were misdiagnosed as "no output", obscuring the real cause ofStop-Job/ Ctrl-C / remoting-disconnect scenarios. - Performance:
Get-AzLocalUpdateSummary,Get-AzLocalClusterUpdateReadiness,Start-AzLocalClusterUpdate,Get-AzLocalUpdateRuns, and the privateGet-AzLocalClusterUpdateRunshelper now accumulate per-cluster results in a[System.Collections.Generic.List[object]](O(1) amortised.Add()) instead of anObject[]with+=(O(n^2) total). Measurable speed-up at fleet scale (1000+ clusters); no API surface change - the functions still return arrays.
- Security:
The jump from 0.6.5 to 0.7.0 is a large, fleet-scale release focused on correctness at 1500+ clusters, true parallel execution, HTML report performance, and a round of security hardening. No breaking public-surface changes.
- HIGH: Azure Resource Graph queries were hardcoded to
az graph query --first 1000. At 1500 clusters, 500 were silently dropped - no error, no warning. New privateInvoke-AzResourceGraphQueryhelper loops on the$skipTokenuntil exhausted. - HIGH:
Invoke-AzLocalFleetOperation -ThrottleLimitpreviously only affected retry-backoff math; the per-cluster loop was fully sequential. At 1500 clusters that meant 4+ hour runs. Extracted the parallelStart-Jobpattern into a shared private helperInvoke-FleetJobsInParalleland rerouted all fleet operations through it.-ThrottleLimitnow controls concurrent API calls (default 4, range 1-16). PowerShell 5.1 compatibility preserved. - HIGH:
Get-AzLocalClusterInventorythrewThe variable cannot be validated because the value '' is not a valid value for the UpdateRingValue variable.whenever a cluster in the fleet was missing theUpdateRingtag. Root cause: the function's[ValidatePattern]parameter$UpdateRingValuecollided with a loop-local$updateRingValue(PowerShell variable names are case-insensitive). Locals renamed to$ringTagValue/$windowTagValue/$exclusionsTagValue.-AllClustersreports now complete against real-world mixed-tag fleets.
- These per-cluster functions now run in parallel batches via the shared helper:
Get-AzLocalClusterUpdateReadiness,Test-AzLocalClusterHealth,Get-AzLocalUpdateSummary,Get-AzLocalAvailableUpdates,Set-AzLocalClusterUpdateRingTag,Get-AzLocalUpdateRuns. Expected 5-10x speedup on 1500-cluster runs (readiness check from ~10 min to ~1-2 min). New-AzLocalFleetStatusHtmlReportrenderer rewritten for O(n) scaling: pre-indexedLatestRunsandClusterDetailshashtables, HTML encoding moved to collection time, per-cluster portal URLs precomputed once. ~60% faster HTML render at 1500 clusters.- HTML report output now written as UTF-8 without BOM via
[System.IO.File]::WriteAllText+UTF8Encoding($false). - New opt-in pass-through parameters (
-UpdateSummary,-AvailableUpdates) so pre-fetched data can be reused across a pipeline, avoiding redundant ARM reads.
New-AzLocalFleetStatusHtmlReport -AllClustersandGet-AzLocalFleetStatusData -AllClusterspreviously truncated at the first 100 clusters silently. The default cap is removed - all discovered clusters are now included. New-MaxClusters <int>parameter (default 0 = no cap, range 1-100000) lets callers optionally trim the slice for targeted runs or testing.
- All
| ConvertFrom-Jsoncall sites outsideInvoke-AzRestJsonaudited - previously any non-JSON ARM response (HTTP 204, error HTML, stray stderr on stdout) would throw uncaught under Strict Mode. - Empty-pipeline guards added to health-failures and latest-run aggregation paths so they no longer silently return
$null. - Update-name sort is now deterministic (secondary sort on
$_.name); unparseable YYMM components log aWarninginstead of silently grouping at position 0. - Parallel CSV log writes: each worker writes a per-job CSV; coordinator merges at the end. Eliminates line interleaving / header corruption that
Add-Contentcannot protect against. - Tag property access is now robust to both
HashtableandPSCustomObjecttag shapes returned by different ARM endpoints. - Malformed
UpdateWindow/UpdateExclusionstag values are now blocking by default (update skipped,Errorlogged) unless-Forceis specified. Previously logged as a warning and the update proceeded.
-UpdateRingValueis whitelist-validated against^[a-zA-Z0-9._-]+$before KQL interpolation in ARG queries.- New private helper
ConvertTo-SafeCsvFieldprefixes formula-leader characters (=,+,-,@, tab) with a single quote and strips embedded CR/LF. Applied uniformly to every field written by the CSV loggers. Prevents Excel formula injection via attacker-controlled cluster name / error message. - User-supplied output paths (
-OutputPath,-ExportResultsPath,-LogFolderPath,-StateFilePath) are resolved via[IO.Path]::GetFullPath(), length-capped at 248 chars, and rejected if they contain..\traversal sequences when a relative root was expected. - Az CLI error output is scrubbed before being written to logs:
--password <value>/--secret <value>echoes masked; token-shaped substrings redacted. Invoke-AzRestJsonhandles mid-run token expiry: on HTTP 401 it runsaz account get-access-tokenonce, refreshes, and retries. Long fleet operations crossing the 1-hour token boundary no longer fail partway through.Stop-AzLocalFleetUpdateandNew-AzLocalFleetStatusHtmlReportnow supportShouldProcess(-WhatIf/-Confirm).
- Breaking for pre-release consumers only (no one was using this yet): the
UpdateWindowAzure resource tag now uses_as the separator between the day-spec and the time range, instead of:. This removes the ambiguity with theHH:MMtime portion and makes the tag easier to read at a glance.- Old:
Mon-Fri:22:00-02:00 - New:
Mon-Fri_22:00-02:00 - Multi-window separator (
;) and day-range separator (-) are unchanged. - The parser in
ConvertFrom-AzLocalUpdateWindowwill throwInvalid window segment syntaxfor the old format; combined with the fail-closed schedule-tag evaluation above, any cluster still carrying the old tag value will have its updates blocked until re-tagged. UseSet-AzLocalClusterUpdateRingTag -UpdateWindowValue 'Mon-Fri_22:00-02:00' -Forceto migrate.
- Old:
- Duration now uses
HH:MM:SSfixed-width format (wasN.N hoursfractional). Easier to read, no loss of precision, survives multi-day runs (52:15:30for 52h 15m). - Attempts are now aggregated per update: when an update runs multiple times on a cluster (a re-run after failure), the report shows one row with
Update Attempts = NandDuration = <sum of all attempts>instead of showing just the last attempt's duration.StartTimereflects the earliest attempt;State/Progress/Current Stepreflect the latest attempt. - New Update Attempts column is shown only when at least one cluster has >1 attempt on its current update, keeping single-attempt fleets uncluttered.
- Only the most-recently-started update per cluster is displayed (one row per cluster); historical update versions from prior cycles are no longer duplicated into separate rows.
- New Current SBE Version column shows the solution-builder-extension version installed on each cluster, alongside the solution update version. Extracted from the
/updatesadditionalProperties.SBEVersionof the most recent applied SBE update and surfaced throughGet-AzLocalFleetStatusDataand the GitHub Actions / Azure DevOps fleet-status pipelines.
-WhatIfoutput is no longer polluted by the module's ownWrite-Log/Write-UpdateCsvLogside effects, internalEnv:cleanup, or log-folder creation. Previously every internal housekeeping line produced aWhat if:row. Now only the actual ARMPOSTapply/actioncall appears in the WhatIf preview.-WhatIfruns (andShouldProcess-declined runs) now count as WouldUpdate in the final summary and are surfaced distinctly fromStarted/Skipped/Failed. Makes dry-runs at fleet scale actually auditable.
- Central helper for duration rendering; accepts
[TimeSpan], numeric seconds, orHH:MM:SSstring. Emits"1 hour 23 minutes"style for the per-runGet-AzLocalUpdateRunsoutput. The fleet HTML report uses its ownHH:MM:SSformatter because it sums across attempts (see above).
- No breaking changes to exported functions or parameter sets. All new helpers are private.
- Az CLI remains the ARM transport for v0.7.0; a native
Invoke-RestMethodport is deferred.
Set-AzLocalClusterUpdateRingTagnow correctly appliesUpdateWindowandUpdateExclusionstags from CSV (HIGH). Inside the processing loop, four references used an undefined variable ($cluster) instead of the actual loop variable ($clusterEntry). BecauseSet-StrictModeis not enforced at module scope, the typo silently returned$null, so:- Clusters with an existing
UpdateRingtag were skipped even when the CSV changedUpdateWindow/UpdateExclusions. - On new/forced writes the PATCH body only contained
UpdateRing;UpdateWindow/UpdateExclusionscolumns from the CSV were never sent to Azure. - Round-trip
Get-AzLocalClusterInventory-> edit CSV ->Set-AzLocalClusterUpdateRingTagnow correctly preserves all three tag columns.
- Clusters with an existing
-
New optional parameters
-UpdateWindowValueand-UpdateExclusionsValueonSet-AzLocalClusterUpdateRingTag -ClusterResourceIds. Direct-invocation mode is now symmetrical with CSV mode and can set all three tags (UpdateRing,UpdateWindow,UpdateExclusions) in a single PATCH operation:Set-AzLocalClusterUpdateRingTag ` -ClusterResourceIds $ids ` -UpdateRingValue 'Wave1' ` -UpdateWindowValue 'Mon-Fri_22:00-02:00' ` -UpdateExclusionsValue '2026-12-20/2026-01-05' -Force
-
Set-StrictMode -Version 1.0is now enforced at module scope. Catches references to uninitialized variables (the exact class of bug fixed above) at runtime instead of silently returning$null. All 239 Pester tests pass unchanged.-Version Latestwas deliberately not selected because ARM REST responses legitimately omit optional properties (e.g.additionalProperties.SBEPublisher,tags.UpdateRing) and Latest would throw on every such dot-notation access.
- New internal function
Test-AzCliAvailable: Checks if Azure CLI (az) is installed before anyazinvocation - In interactive sessions, prompts the user to download and install when
azis not found - In non-interactive environments (CI/CD pipelines), throws immediately with clear installation instructions
- All exported functions and SingleCluster code paths now call
Test-AzCliAvailablebefore firstazCLI usage
- New function
Get-AzLocalFleetStatusData: Single-pass data collection with parallelStart-Jobsupport -ThrottleLimitparameter (default: 4, max: 8) splits cluster list into parallel batches-ExportPathexports fleet data as JSON artifact for CI/CD pipeline job passing-StatusDataparameter onNew-AzLocalFleetStatusHtmlReportaccepts pre-collected data to skip API calls- Stable JSON schema (v1.0) with SchemaVersion, Timestamp, ModuleVersion, Scope, Readiness, ClusterDetails, LatestRuns, HealthResults
- All per-update state filters now use module-level constants (
$script:ReadyStates,$script:PrereqStates) aligned with current ARM API states ReadyToInstallstate is now recognized alongsideReadyacross all functions:Start-AzLocalClusterUpdate,Get-AzLocalAvailableUpdates,Get-AzLocalClusterUpdateReadiness,Get-AzLocalFleetStatusData,Get-AzLocalUpdateSummary- Update summary state checks include
ReadyToInstallfor accurate "Update Available" counting
Get-AzLocalAvailableUpdates: Now shows HasPrerequisite/AdditionalContentRequired counts alongside Ready counts in console output (both single-cluster and multi-cluster modes)Get-AzLocalAvailableUpdates: Result objects includePackageTypeandSBEDependencyproperties for updates blocked by SBE prerequisitesGet-AzLocalAvailableUpdates: Summary section shows clusters blocked by SBE prerequisites with vendor dependency details (Publisher, Family, ReleaseNotes)Get-AzLocalAvailableUpdates: New-Rawswitch returns unprocessed ARM API objects for programmatic use (internal callers use this automatically)Start-AzLocalClusterUpdate: Provides detailed SBE dependency info when updates are blocked by HasPrerequisite/AdditionalContentRequired state, with guidance to install the SBE from the hardware vendorGet-AzLocalClusterUpdateReadiness: SurfacesHasPrerequisiteUpdatesandSBEDependencyin result objects for downstream consumptionGet-AzLocalClusterUpdateReadiness: Console output shows "Has Prerequisite (SBE update required)" for clusters with only prerequisite-blocked updatesGet-AzLocalClusterUpdateReadiness: Summary section includes count of clusters blocked by SBE prerequisites with vendor-specific guidanceGet-AzLocalFleetStatusData: Sequential collection now extracts HasPrerequisite and SBE dependency info into readiness dataGet-AzLocalFleetStatusData: Status output shows "Has Prerequisite" for clusters with only prerequisite-blocked updates
- New exported function
Test-AzLocalUpdateScheduleAllowed: Master gate that evaluatesUpdateWindowandUpdateExclusionsAzure resource tags to determine if an update should proceed - New internal function
ConvertFrom-AzLocalUpdateWindow: Parses maintenance window tag syntax (<days>_<HH:MM>-<HH:MM>) including day ranges, wildcards (*/Daily), and overnight windows - New internal function
ConvertFrom-AzLocalUpdateExclusion: Parses exclusion/blackout period tag syntax (YYYY-MM-DD/YYYY-MM-DD) with wildcard year support for recurring patterns Start-AzLocalClusterUpdatenow checks schedule tags before applying updates; returnsScheduleBlockedstatus when outside maintenance windows or during exclusion periods- Exclusion periods take priority over maintenance windows
New-AzLocalFleetStatusHtmlReportnow uses single-pass data collection instead of calling 6 separate module functions- Reduced Azure REST API calls from ~230 to ~85 for 21 clusters (~63% reduction)
- ByTag scope resolves resource IDs upfront via single ARG query instead of each downstream function querying independently
- Update summary, available updates, and health check data fetched once per cluster and reused
- Progress counter shows
[N/M]per cluster during data collection for better visibility
- Apply Updates pipelines: Summary now includes
ScheduleBlockedcount alongside Started/Skipped/Failed/HealthBlocked; adds "Actions Required" section with remediation guidance - Fleet Update Status pipelines: HasPrerequisite clusters now appear as
Failed (HasPrerequisite)in JUnit XML instead of silently passing; SBE vendor details shown in test output - Fleet Status JSON: Summary block now includes
HasPrerequisitecount as a distinct metric (previously lumped intoNotReady) - Fleet Status summaries: Both GitHub Actions and Azure DevOps summaries now show
SBE Prerequisite Blockedrow and "Actions Required" section - Readiness CSV/JSON:
UpdateWindowandUpdateExclusionstag values now included in readiness output, so ops teams can see which clusters have schedule restrictions
Get-AzLocalClusterInfo,Invoke-AzLocalUpdateApply, and SingleCluster paths inGet-AzLocalUpdateSummary,Get-AzLocalAvailableUpdates,Get-AzLocalUpdateRunshad noazCLI availability check - previously threw unhelpfulCommandNotFoundException- Existing auth check catch blocks now differentiate 'az not installed' from 'az not logged in' with distinct error messages
- 'Up to Date' counter now recognizes
AppliedSuccessfullystate from ARM API (was showing 0 for completed clusters) - Recommended Update no longer shows the version a cluster is already on when state is
AppliedSuccessfully/UpToDate
- Fixed
-PassThruparameter onGet-AzLocalUpdateSummary(was missing from param declaration) -OutputPathnow pre-validated upfront (drive existence, .html extension) to fail fast before API calls- Portal URLs in HTML report now HTML-encoded to prevent attribute injection
- ARG KQL queries now escape single quotes in
UpdateRingValueto prevent injection - All dynamic HTML values consistently HTML-encoded
Get-CurrentStepPathhas MaxDepth=20 safety limit- Cluster name matching uses exact segment comparison instead of suffix pattern
- New function
New-AzLocalFleetStatusHtmlReportgenerates self-contained HTML reports for fleet update status - Collects readiness, update summaries, available updates, health checks, and update run history into a single report
- Executive summary cards with color-coded progress bar showing fleet-wide update adoption
- Cluster Information section with name, current version, node count, resource group, resource ID
- Cluster Status Details with Active Update column (shows in-progress/failed update) and Recommended Update
- Recent Update Run History with recursive Current Step traversal (up to 8+ levels deep)
- Health Check Failures with severity filter (Critical/Warning/Informational) and collapsible per-cluster groups for multi-cluster reports
- Azure Local purple gradient design with embedded Azure Local instance logo
-AllClustersswitch discovers all clusters via ARG (v0.6.2 capped at 100; v0.7.0 removes the cap - use-MaxClustersto trim); auto-generates title from cluster name for single-cluster reports- Supports all input methods:
-ClusterResourceIds,-ClusterNames,-ScopeByUpdateRingTag,-AllClusters - Use
-PassThruto capture the HTML string for email body or further processing
# Generate HTML report for a single cluster (auto-titles as "Seattle - Update Status Report")
New-AzLocalFleetStatusHtmlReport -ClusterNames Seattle `
-OutputPath "C:\Reports\seattle.html" -IncludeHealthDetails -IncludeUpdateRuns
# Generate report for all clusters across the subscription (uncapped by default; use -MaxClusters to trim)
New-AzLocalFleetStatusHtmlReport -AllClusters `
-OutputPath "C:\Reports\fleet-all.html" -IncludeHealthDetails -IncludeUpdateRuns
# Generate report for all Wave1 clusters
New-AzLocalFleetStatusHtmlReport -ScopeByUpdateRingTag -UpdateRingValue "Wave1" `
-OutputPath "C:\Reports\wave1-status.html" -IncludeHealthDetails -IncludeUpdateRuns- All functions that accept
-ClusterNamesnow resolve names to resource IDs once upfront - Eliminates redundant API calls when multiple functions are called sequentially
- CI/CD pipelines use
-ClusterResourceIdsconsistently (reduces ~800 to ~300 API calls for 100 clusters) Test-AzLocalClusterHealthaccepts-UpdateSummaryto skip redundant fetch
- New function
Test-AzLocalClusterHealthvalidates cluster health before applying updates - Queries health check results from ARM (
updateSummariesresource) to identify Critical, Warning, and Informational failures - Critical failures block updates from being applied - this function shows you exactly what needs fixing
- Supports
-BlockingOnlyto show only update-blocking issues - Export results to CSV, JSON, or JUnit XML for CI/CD integration
- Before applying an update, the function now automatically checks for Critical health failures (Step 3b)
- If blocking issues are found, the cluster is skipped with detailed failure information and remediation guidance
- No more cryptic "Update is blocked due to health check failure" errors without context
- When the latest update run failed due to health check failures, the function automatically queries and displays the Critical failures
- Shows remediation steps inline so you know exactly what to fix
- Functions no longer dump object lists to the console by default
- Formatted tables and diagnostics are still displayed β only the raw object output is suppressed
- Use
-PassThruto return objects for pipeline/variable capture:$results = Start-AzLocalClusterUpdate ... -PassThru - CI/CD pipeline examples updated accordingly
Six new functions for managing updates across fleets of 1000-3000+ clusters:
| Function | Description |
|---|---|
Invoke-AzLocalFleetOperation |
Orchestrates fleet-wide updates with batching, throttling, and retry logic |
Get-AzLocalFleetProgress |
Real-time progress tracking with success/failure percentages |
Test-AzLocalFleetHealthGate |
CI/CD health gate to prevent cascading failures between waves |
Export-AzLocalFleetState |
Save operation state for resume capability |
Resume-AzLocalFleetUpdate |
Resume interrupted operations from checkpoint |
Stop-AzLocalFleetUpdate |
Graceful stop with state preservation |
Enterprise-Scale Features:
- π¦ Batch Processing: Process clusters in configurable batches (default: 50)
- π Retry Logic: Automatic retries with exponential backoff (default: 3 retries)
- πΎ State Management: Checkpoint/resume capability for long-running operations
- π¦ Health Gates: Configurable thresholds (default: max 5% failure, min 90% success)
- π Progress Tracking: Real-time visibility into fleet-wide operations
Examples:
# Start fleet-wide update with batching
Invoke-AzLocalFleetOperation -ScopeByUpdateRingTag -UpdateRingValue "Production" `
-BatchSize 100 -ThrottleLimit 20 -Force
# Check progress during operation
Get-AzLocalFleetProgress -ScopeByUpdateRingTag -UpdateRingValue "Production" -Detailed
# CI/CD health gate between waves
$gate = Test-AzLocalFleetHealthGate -ScopeByUpdateRingTag -UpdateRingValue "Wave1" `
-MaxFailurePercent 2 -WaitForCompletion
if (-not $gate.Passed) { exit 1 }
# Resume after interruption
Resume-AzLocalFleetUpdate -StateFilePath "C:\Logs\fleet-state.json" -RetryFailed -ForceThree functions now support tag-based filtering for fleet-wide operations:
| Function | New Capabilities |
|---|---|
Get-AzLocalUpdateSummary |
Query summaries across fleet by tag, name, or resource ID |
Get-AzLocalAvailableUpdates |
List available updates across fleet by tag, name, or resource ID |
Get-AzLocalUpdateRuns |
Get update run history across fleet by tag, name, or resource ID |
New Parameters for All Three Functions:
-ClusterNames/-ClusterResourceIds- Query multiple specific clusters-ScopeByUpdateRingTag+-UpdateRingValue- Query clusters by UpdateRing tag-ExportPath- Export results to CSV, JSON, or JUnit XML (format auto-detected from extension)
Examples:
# Get update summaries for all Wave1 clusters
Get-AzLocalUpdateSummary -ScopeByUpdateRingTag -UpdateRingValue "Wave1"
# List available updates across Production clusters, export to CSV
Get-AzLocalAvailableUpdates -ScopeByUpdateRingTag -UpdateRingValue "Production" -ExportPath "updates.csv"
# Get latest update run from all Ring2 clusters
Get-AzLocalUpdateRuns -ScopeByUpdateRingTag -UpdateRingValue "Ring2" -Latest- New CI/CD Pipeline: Added
Step.6_fleet-update-status.ymlfor both GitHub Actions and Azure DevOps - JUnit XML Reports: Each cluster appears as a test case in CI/CD dashboards (passed=healthy, failed=issues)
- Multiple Output Formats: CSV, JSON, and JUnit XML exports for different use cases
- Scheduled Monitoring: Automated daily checks at 6 AM UTC with configurable scope
- Dashboard Integration: Results appear in GitHub Actions Tests tab and Azure DevOps Tests tab with analytics
- Consistent Logging: All functions now use
Write-Logfor consistent, timestamped, colored console output - Improved Progress Visibility:
Get-AzLocalUpdateRuns,Get-AzLocalClusterUpdateReadiness, andGet-AzLocalClusterInventorynow show detailed progress during API operations - File Logging Support: When
$script:LogFilePathis set, all functions write to log files - Severity Levels: Messages use appropriate levels (Info=White, Warning=Yellow, Error=Red, Success=Green, Header=Cyan)
- OpenID Connect (OIDC) Documentation: Added comprehensive guidance for secretless authentication using federated credentials
- Authentication Best Practices: Documented three authentication methods ranked by security (OIDC > Managed Identity > Client Secret)
- CI/CD Pipeline Updates: All GitHub Actions workflows now default to OIDC authentication with
id-token: writepermission - Azure DevOps Guidance: Added Workload Identity Federation setup instructions
- Added authentication method comparison table with security ratings
- Updated Quick Start guide with OIDC examples
- Added links to Microsoft documentation for federated credentials setup
- Documented subject claim patterns for GitHub Actions (branch, PR, environment, tag)
- Verified and documented that all functions work with all three authentication methods:
- Interactive - Standard user login via
az login - Service Principal - For CI/CD pipelines using
Connect-AzLocalServicePrincipal - Managed Identity (MSI) - For Azure-hosted agents using
Connect-AzLocalServicePrincipal -UseManagedIdentity
- Interactive - Standard user login via
- Managed Identity (MSI) Support:
Connect-AzLocalServicePrincipalnow supports Managed Identity authentication with-UseManagedIdentityswitch, ideal for Azure-hosted runners, VMs, and containers
- CRITICAL: Fixed Azure Resource Graph queries in
Get-AzLocalClusterInventory,Start-AzLocalClusterUpdate, andGet-AzLocalClusterUpdateReadinessthat were returning incorrect resource types (mixed resources like networkInterfaces, virtualHardDisks instead of clusters only). The issue was caused by HERE-STRING query format causing malformed az CLI commands. Queries now use single-line string format. - CRITICAL: Fixed
Set-AzLocalClusterUpdateRingTagfailing with JSON deserialization errors when applying tags. Two issues were resolved:- PowerShell/cmd.exe mangling JSON quotes when passed to
az rest --body- now uses temp file with@filesyntax - PowerShell hashtable internal properties (
Keys,Values, etc.) being included in JSON - now uses[PSCustomObject]with filteredNotePropertymembers only
- PowerShell/cmd.exe mangling JSON quotes when passed to
Get-AzLocalClusterInventoryno longer dumps objects to console when using-ExportPath(cleaner output with summary and next steps)- Added
-PassThruswitch toGet-AzLocalClusterInventoryfor CI/CD pipelines that need both CSV export AND returned objects
- Cluster Inventory Function: New
Get-AzLocalClusterInventoryfunction queries all clusters and their UpdateRing tag status - CSV-Based Tag Workflow: Export inventory to CSV, edit UpdateRing values in Excel, then import back to apply tags
- CSV Input for Tags:
Set-AzLocalClusterUpdateRingTagnow accepts-InputCsvPathfor bulk tag operations - JUnit XML Export for CI/CD: Export results to JUnit XML format for visualization in Azure DevOps, GitHub Actions, Jenkins, and other CI/CD tools
- Renamed
-ScopeByTagNameto-ScopeByUpdateRingTag(now a switch parameter for clarity) - Renamed
-TagValueto-UpdateRingValuefor consistency - UpdateRing tag queries now use the standardized 'UpdateRing' tag name
-ExportResultsPathand-ExportPathnow support.xmlextension for JUnit format
