diff --git a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/README.md b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/README.md index 2ed1e66a..dea2ac14 100644 --- a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/README.md +++ b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/README.md @@ -64,7 +64,7 @@ By the end of this guide you will have: - **Fleet Connectivity Status** (Step.4, v0.7.79+, enhanced in v0.7.85) - read-only daily snapshot of Arc agent connectivity, physical NIC health, Azure Resource Bridge status, and the node-count reconciliation between cluster `reportedProperties.nodes` and Arc-tagged physical machines. *Scheduled daily 05:30 UTC + manual.* - **Assess Update Readiness** (Step.5) - pre-flight, report-only readiness + blocking-health snapshot, published as JUnit XML. *Manual only.* - **Sideload Updates** (Step.6, v0.8.7) - **opt-in, off by default, on-prem self-hosted runner/agent required.** Pre-stages solution-update media onto clusters that cannot pull updates from Azure directly (dark / air-gapped fabrics): Robocopy to the cluster import share, remote SHA256 verify, `Add-SolutionUpdate` import, then flip `UpdateSideloaded=True` for the downstream Step.7 apply. Re-entrant state machine driven by a frequent CRON; the multi-hour copy runs in a detached Scheduled Task. *Inert unless `SIDELOAD_UPDATES=true`. See [sideload.md](docs/sideload.md) and [sideload-robocopy.md](docs/sideload-robocopy.md).* - - **Apply Updates** (Step.7) - apply updates to a single `UpdateRing` wave at a time, with WhatIf / dry-run support. *Manual only by default - **you must add a schedule** that lines up with your cluster `UpdateStartWindow` tags, see [Step 7 - Apply Updates](docs/appendix-pipelines.md#step-7---apply-updates) and [section 8](#8-scheduling-maintenance-windows-and-change-freeze-periods).* + - **Apply Updates** (Step.7; **v0.8.78 step-summary UX polish**: ``ScheduleBlocked`` / ``SideloadedBlocked`` / ``ExcludedByTag`` outcomes render as JUnit ```` instead of ```` so ``dorny/test-reporter`` no longer flips Step.07 RED on by-design gate-respect outcomes - ``HealthCheckBlocked`` deliberately stays a ````; the Readiness KPI table now also surfaces **Already Up to Date** and **Not Ready (needs attention before updating)** rows so operators see the full ring breakdown - eligible, started, skipped, blocked, up-to-date, needs-attention - in one place; GHA-only: ``actions/download-artifact@v6 -> @v7`` silences the Node 20 deprecation warning) - apply updates to a single `UpdateRing` wave at a time, with WhatIf / dry-run support. *Manual only by default - **you must add a schedule** that lines up with your cluster `UpdateStartWindow` tags, see [Step 7 - Apply Updates](docs/appendix-pipelines.md#step-7---apply-updates) and [section 8](#8-scheduling-maintenance-windows-and-change-freeze-periods).* - **Monitor In-Flight Updates** (Step.8, v0.7.90; v0.7.96 surfaces `Status` + deepest `ErrorMessage` columns, adds the `StepError` stuck-step JUnit type for runs that have hit an error inside a step without crossing the long-running threshold, and renders portal-linked Cluster Name / Update Name cells in the markdown summary; **v0.7.98 UX overhaul**: composite `SeverityScore` sort, per-cell `StateIcon` + `StatusIcon`, horizontal chip stack (`STEP-STUCK` / `RUN-STUCK` / `UNRESOLVED` / `RECENT-FAIL`), `CRITICAL / WARN / OK` fleet status badge at the top of the job summary, collapsible `
` Verbose Error block per row, and JUnit `` + `` populated with real run elapsed seconds). Operational snapshot during an active wave: lists each cluster whose latest update run is `InProgress`, with current step, progress (`completed/total steps`), elapsed duration, the `Status` column (`Success`/`Error`/`InProgress`/...), and the deepest `errorMessage` walked out of the nested ARM `steps[]` tree; flags long-running runs (default >6h) AND step-errored stuck runs as JUnit failures in the Checks tab. *Scheduled 5x/day at 20:00, 22:00, 00:00, 02:00, 04:00 UTC (every 2h across the typical overnight maintenance window) + manual; default cadence is editable in `monitor-updates.yml` (v0.7.92+).* - **Fleet Update Status** (Step.9, formerly Step.8; v0.7.96 promotes `NeedsAttention` into the **Update Failed** bucket, adds a new **Action Required** bucket for `PreparationFailed`, and folds `PreparationInProgress` into **Update In Progress**; **v0.7.98** populates JUnit `time=` on each `` in the `📜 Update Run History and Error Details` testsuite using `DurationMinutes * 60`). Scheduled daily snapshot of fleet update state, surfaced in the Tests tab. Markdown summary's `📜 Update Run History and Error Details` table now also carries portal-linked Cluster Name / Update Name cells plus the deepest-step `ErrorMessage`. *Scheduled daily 06:00 UTC + manual.* - **Fleet Health Status** (Step.10, formerly Step.9) - scheduled daily snapshot of 24-hour system health-check failures, surfaced in the Tests tab. *Scheduled daily 07:00 UTC + manual.* diff --git a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/apply-updates-schedule-audit.yml b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/apply-updates-schedule-audit.yml index bacf8feb..694d3d47 100644 --- a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/apply-updates-schedule-audit.yml +++ b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/apply-updates-schedule-audit.yml @@ -87,7 +87,7 @@ parameters: default: false variables: - GENERATED_AGAINST_MODULE_VERSION: '0.8.77' + GENERATED_AGAINST_MODULE_VERSION: '0.8.78' REQUIRED_MODULE_VERSION: '${{ parameters.moduleVersion }}' reportsPath: '$(Build.ArtifactStagingDirectory)/reports' # v0.8.7 sideload advisor defaults. Override at the pipeline / variable-group diff --git a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/apply-updates.yml b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/apply-updates.yml index 3053981c..12af9a5e 100644 --- a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/apply-updates.yml +++ b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/apply-updates.yml @@ -120,7 +120,7 @@ variables: # log if the YAML appears stale - prompting you to refresh via # Copy-AzLocalPipelineExample -Update. See Automation-Pipeline-Examples/README.md section 5. - name: GENERATED_AGAINST_MODULE_VERSION - value: '0.8.77' + value: '0.8.78' # Resolution order for the module version pin (leave all unset to install the latest, # which is the default "fix-forward" behaviour): queue-time parameter > pipeline variable # 'REQUIRED_MODULE_VERSION' overridden at queue time > empty (latest). @@ -276,6 +276,14 @@ stages: variables: readyCount: $[ stageDependencies.CheckReadiness.ReadinessCheck.outputs['readiness.ReadyCount'] ] totalCount: $[ stageDependencies.CheckReadiness.ReadinessCheck.outputs['readiness.TotalCount'] ] + # v0.8.78: surface the readiness gate's Up-to-Date / Not-Ready breakdown + # in the apply-updates summary so the operator sees the FULL ring picture + # (not just the clusters that entered Apply Updates). Up-to-Date is a + # healthy steady state; Not-Ready includes clusters held back by a + # previously-failed run (state=NeedsAttention / UpdateFailed), an + # in-progress run, pending SBE prerequisites or a critical health failure. + upToDateCount: $[ stageDependencies.CheckReadiness.ReadinessCheck.outputs['readiness.UpToDateCount'] ] + notReadyCount: $[ stageDependencies.CheckReadiness.ReadinessCheck.outputs['readiness.NotReadyCount'] ] # pull the upstream artifact-stamp through so the download step # can reconstruct the timestamped artifact name produced by CheckReadiness. readinessArtifactStamp: $[ stageDependencies.CheckReadiness.ReadinessCheck.outputs['stamp.artifactStamp'] ] @@ -486,6 +494,11 @@ stages: UpdateRing = $env:RESOLVED_UPDATE_RING TotalCount = "$(totalCount)" ReadyCount = "$(readyCount)" + # v0.8.78: pass the readiness gate's Up-to-Date / Not-Ready breakdown + # so the Readiness KPI surfaces the FULL ring picture, not just the + # clusters that entered Apply Updates. + UpToDateCount = "$(upToDateCount)" + NotReadyCount = "$(notReadyCount)" Succeeded = "$(applyUpdates.Succeeded)" Skipped = "$(applyUpdates.Skipped)" Failed = "$(applyUpdates.Failed)" diff --git a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/assess-update-readiness.yml b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/assess-update-readiness.yml index 12edf9b9..9bc0fa41 100644 --- a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/assess-update-readiness.yml +++ b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/assess-update-readiness.yml @@ -70,7 +70,7 @@ variables: # the version actually installed and to the latest on PSGallery, and emits a warning # log if the YAML appears stale - prompting you to refresh via # Copy-AzLocalPipelineExample -Update. See Automation-Pipeline-Examples/README.md section 5. - GENERATED_AGAINST_MODULE_VERSION: '0.8.77' + GENERATED_AGAINST_MODULE_VERSION: '0.8.78' # Resolution order for the module version pin (leave all unset to install the latest, # which is the default "fix-forward" behaviour): queue-time parameter > pipeline variable # 'REQUIRED_MODULE_VERSION' overridden at queue time > empty (latest). diff --git a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/authentication-test.yml b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/authentication-test.yml index d6c94c03..569f0ce9 100644 --- a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/authentication-test.yml +++ b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/authentication-test.yml @@ -60,7 +60,7 @@ variables: # log if the YAML appears stale - prompting you to refresh via # Copy-AzLocalPipelineExample -Update. See Automation-Pipeline-Examples/README.md section 5. - name: GENERATED_AGAINST_MODULE_VERSION - value: '0.8.77' + value: '0.8.78' # Resolution order for the module version pin (leave all unset to install the latest, # which is the default "fix-forward" behaviour): queue-time parameter > pipeline variable # 'REQUIRED_MODULE_VERSION' overridden at queue time > empty (latest). diff --git a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/fleet-connectivity-status.yml b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/fleet-connectivity-status.yml index 1dc7c7ba..e6ade4f7 100644 --- a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/fleet-connectivity-status.yml +++ b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/fleet-connectivity-status.yml @@ -110,7 +110,7 @@ variables: # the version actually installed and to the latest on PSGallery, and emits a warning # log if the YAML appears stale - prompting you to refresh via # Copy-AzLocalPipelineExample -Update. See Automation-Pipeline-Examples/README.md section 5. - GENERATED_AGAINST_MODULE_VERSION: '0.8.77' + GENERATED_AGAINST_MODULE_VERSION: '0.8.78' # Resolution order for the module version pin (leave all unset to install the latest, # which is the default "fix-forward" behaviour): queue-time parameter > pipeline variable # 'REQUIRED_MODULE_VERSION' overridden at queue time > empty (latest). diff --git a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/fleet-health-status.yml b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/fleet-health-status.yml index 56954c34..96b84a77 100644 --- a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/fleet-health-status.yml +++ b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/fleet-health-status.yml @@ -105,7 +105,7 @@ variables: # the version actually installed and to the latest on PSGallery, and emits a warning # log if the YAML appears stale - prompting you to refresh via # Copy-AzLocalPipelineExample -Update. See Automation-Pipeline-Examples/README.md section 5. - GENERATED_AGAINST_MODULE_VERSION: '0.8.77' + GENERATED_AGAINST_MODULE_VERSION: '0.8.78' # Resolution order for the module version pin (leave all unset to install the latest, # which is the default "fix-forward" behaviour): queue-time parameter > pipeline variable # 'REQUIRED_MODULE_VERSION' overridden at queue time > empty (latest). diff --git a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/fleet-update-status.yml b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/fleet-update-status.yml index ada4b3dc..a0db44af 100644 --- a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/fleet-update-status.yml +++ b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/fleet-update-status.yml @@ -93,7 +93,7 @@ variables: # the version actually installed and to the latest on PSGallery, and emits a warning # log if the YAML appears stale - prompting you to refresh via # Copy-AzLocalPipelineExample -Update. See Automation-Pipeline-Examples/README.md section 5. - GENERATED_AGAINST_MODULE_VERSION: '0.8.77' + GENERATED_AGAINST_MODULE_VERSION: '0.8.78' # Resolution order for the module version pin (leave all unset to install the latest, # which is the default "fix-forward" behaviour): queue-time parameter > pipeline variable # 'REQUIRED_MODULE_VERSION' overridden at queue time > empty (latest). diff --git a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/inventory-clusters.yml b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/inventory-clusters.yml index 9f840c0e..55156393 100644 --- a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/inventory-clusters.yml +++ b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/inventory-clusters.yml @@ -43,7 +43,7 @@ variables: # log if the YAML appears stale - prompting you to refresh via # Copy-AzLocalPipelineExample -Update. See Automation-Pipeline-Examples/README.md section 5. - name: GENERATED_AGAINST_MODULE_VERSION - value: '0.8.77' + value: '0.8.78' # Resolution order for the module version pin (leave all unset to install the latest, # which is the default "fix-forward" behaviour): queue-time parameter > pipeline variable # 'REQUIRED_MODULE_VERSION' overridden at queue time > empty (latest). diff --git a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/manage-updatering-tags.yml b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/manage-updatering-tags.yml index 9f37e9ac..80e87e01 100644 --- a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/manage-updatering-tags.yml +++ b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/manage-updatering-tags.yml @@ -46,7 +46,7 @@ variables: # log if the YAML appears stale - prompting you to refresh via # Copy-AzLocalPipelineExample -Update. See Automation-Pipeline-Examples/README.md section 5. - name: GENERATED_AGAINST_MODULE_VERSION - value: '0.8.77' + value: '0.8.78' # Resolution order for the module version pin (leave all unset to install the latest, # which is the default "fix-forward" behaviour): queue-time parameter > pipeline variable # 'REQUIRED_MODULE_VERSION' overridden at queue time > empty (latest). diff --git a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/monitor-updates.yml b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/monitor-updates.yml index 3a718513..9b1ebdfc 100644 --- a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/monitor-updates.yml +++ b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/monitor-updates.yml @@ -84,7 +84,7 @@ parameters: default: '' variables: - GENERATED_AGAINST_MODULE_VERSION: '0.8.77' + GENERATED_AGAINST_MODULE_VERSION: '0.8.78' REQUIRED_MODULE_VERSION: '${{ parameters.moduleVersion }}' reportsPath: '$(Build.ArtifactStagingDirectory)/reports' diff --git a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/sideload-updates.yml b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/sideload-updates.yml index 591ada71..274654ee 100644 --- a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/sideload-updates.yml +++ b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/sideload-updates.yml @@ -79,7 +79,7 @@ variables: # the version actually installed and to the latest on PSGallery, and emits a warning # log if the YAML appears stale - prompting you to refresh via # Copy-AzLocalPipelineExample -Update. See Automation-Pipeline-Examples/README.md section 5. - GENERATED_AGAINST_MODULE_VERSION: '0.8.77' + GENERATED_AGAINST_MODULE_VERSION: '0.8.78' # Resolution order for the module version pin (leave all unset to install the latest, # which is the default "fix-forward" behaviour): queue-time parameter > pipeline variable # 'REQUIRED_MODULE_VERSION' overridden at queue time > empty (latest). diff --git a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/docs/appendix-pipelines.md b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/docs/appendix-pipelines.md index 7dcee952..0629fd06 100644 --- a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/docs/appendix-pipelines.md +++ b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/docs/appendix-pipelines.md @@ -160,6 +160,8 @@ The table below is the ground truth for what each shipped YAML does **out of the > **Behaviour change in v0.8.74**: the pre-apply readiness gate report (`Export-AzLocalClusterReadinessGateReport`) replaces its binary `Ready?` column with a readable `Status` column so fully-patched clusters now show **Up to Date** instead of a no-entry icon that implied failure. The header line shows a distinct `Up to Date` count and the step emits a new additive `UP_TO_DATE_COUNT` / `UpToDateCount` output (the `READY_COUNT` gate that drives the apply is unchanged). Classification is shared with Step 5 and Step 9 via the `Get-AzLocalClusterReadinessStatus` helper. When the gate finds no clusters ready, the "No Clusters Ready" summary (`Add-AzLocalNoReadyClustersStepSummary`) now renders an Up-to-Date vs Not-Ready breakdown - so an idle run makes clear that already-patched clusters are a healthy steady state (a notice, not a warning, when every cluster is up to date) rather than a fault. +> **Behaviour change in v0.8.78**: per-cluster `ScheduleBlocked` / `SideloadedBlocked` / `ExcludedByTag` outcomes now render as JUnit `` instead of `` so `dorny/test-reporter` no longer flips the Step.07 check RED on by-design gate-respect outcomes - `HealthCheckBlocked` deliberately stays a `` because a critical health failure IS actionable. The Step 7 Readiness KPI table also gains optional **Already Up to Date** and **Not Ready (needs attention before updating)** rows, wired in both shipped `apply-updates.yml` templates from the upstream `readiness.UpToDateCount` / `readiness.NotReadyCount` outputs (emitted since v0.8.74), so operators see the full ring breakdown - eligible / started / skipped / blocked / up-to-date / needs-attention - in one place. GitHub Actions only: `actions/download-artifact@v6 -> @v7` to silence the Node 20 deprecation warning (v7 chosen over v8 because v8 has a breaking `digest-mismatch=error` default). + | Aspect | Value | |---|---| | **Purpose** | Apply updates to clusters filtered by `UpdateRing` tag value. The actual mutation step; every other pipeline in this folder is either preparation, monitoring, or reporting. | @@ -170,7 +172,7 @@ The table below is the ground truth for what each shipped YAML does **out of the | **Artefacts** | `update-results.xml` (JUnit, one cluster per test), `update-logs/*` (CSV + detail). When ITSM is enabled: `itsm-results.csv`, `itsm-results.xml`. | | **When to run** | During the maintenance window for each ring, after the readiness assessment is reviewed. | | **RBAC** | Write to clusters required. Covered by the `Azure Stack HCI Update Operator (custom)` custom role (`Microsoft.AzureStackHCI/clusters/updates/apply/action` + cluster-update reads). | -| **Exit conditions** | Pipeline run is green when every in-scope cluster either succeeds or is correctly classified as `ScheduleBlocked` / `SideloadedBlocked` / `ExcludedByTag` (these are skips, not failures). Per-cluster update failures surface as JUnit `` entries; long-running runs are tracked by Step 8. | +| **Exit conditions** | Pipeline run is green when every in-scope cluster either succeeds or is correctly classified as `ScheduleBlocked` / `SideloadedBlocked` / `ExcludedByTag` (these are skips, not failures - rendered as JUnit `` from v0.8.78). Per-cluster update failures and `HealthCheckBlocked` outcomes surface as JUnit `` entries; long-running runs are tracked by Step 8. | | **ITSM** | Supported (opt-in via `raise_itsm_ticket=true` / `raiseItsmTicket=true`). When enabled, one ServiceNow incident is raised per cluster whose update run finished with a failure state; classification skips (`ScheduleBlocked` / `SideloadedBlocked` / `ExcludedByTag`) do not generate tickets. `itsm_dry_run` builds payloads without creating tickets; `itsm_force_create` bypasses dedupe. | > **MANDATORY CUSTOMISATION: the Apply Updates pipeline does not ship with a schedule.** The cluster `UpdateStartWindow` / `UpdateExclusionsWindow` tags **only gate updates *while the pipeline is already running***; they do **not** start the pipeline. If you (a) use `UpdateStartWindow` tags to define when updates may be installed and (b) leave the shipped `apply-updates.yml` with `workflow_dispatch` only (GH) / `trigger: none` (ADO), **no updates will ever be applied automatically** - the pipeline will simply never start during the window. diff --git a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/apply-updates-schedule-audit.yml b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/apply-updates-schedule-audit.yml index cb47e0de..9a1ab300 100644 --- a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/apply-updates-schedule-audit.yml +++ b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/apply-updates-schedule-audit.yml @@ -96,7 +96,7 @@ env: # this to the version actually installed and to the latest on PSGallery, and emits a # ::notice annotation if the YAML appears stale - prompting you to refresh via # Copy-AzLocalPipelineExample -Update. See Automation-Pipeline-Examples/README.md section 5. - GENERATED_AGAINST_MODULE_VERSION: '0.8.77' + GENERATED_AGAINST_MODULE_VERSION: '0.8.78' REQUIRED_MODULE_VERSION: ${{ github.event.inputs.module_version || vars.REQUIRED_MODULE_VERSION || '' }} # v0.8.4 - opt this workflow into Node.js 24 for all JavaScript actions # (actions/checkout, actions/download-artifact, actions/upload-artifact, diff --git a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/apply-updates.yml b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/apply-updates.yml index 232c5b14..64dce1b7 100644 --- a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/apply-updates.yml +++ b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/apply-updates.yml @@ -135,7 +135,7 @@ env: # this to the version actually installed and to the latest on PSGallery, and emits a # ::notice annotation if the YAML appears stale - prompting you to refresh via # Copy-AzLocalPipelineExample -Update. See Automation-Pipeline-Examples/README.md section 5. - GENERATED_AGAINST_MODULE_VERSION: '0.8.77' + GENERATED_AGAINST_MODULE_VERSION: '0.8.78' # Resolution order for the module version pin (leave all unset to install the latest, # which is the default "fix-forward" behaviour): manual workflow_dispatch input > # repository variable 'REQUIRED_MODULE_VERSION' > empty (latest). @@ -354,7 +354,10 @@ jobs: # health, Step 3c schedule, Step 3b1 sideloaded) as defence in depth in # case state drifts between the two jobs. - name: Download Readiness Report - uses: actions/download-artifact@v6 + # v0.8.78: bumped from @v6 to @v7. v6 still ran on Node.js 20 by default + # and triggered the runner's "Node.js 20 is deprecated" warning on every + # apply-updates run. @v7 runs natively on Node 24 and silences the warning. + uses: actions/download-artifact@v7 with: # artifact name now carries a UTC timestamp - resolve via the # upstream job's outputs.readiness_artifact so re-runs on the same day @@ -502,6 +505,13 @@ jobs: UpdateRing = $env:INPUT_UPDATE_RING TotalCount = "${{ needs.check-readiness.outputs.total_count }}" ReadyCount = "${{ needs.check-readiness.outputs.ready_count }}" + # v0.8.78: surface the full ring picture (Up to Date + Not Ready) in + # the apply-updates Readiness KPI, not just the clusters that entered + # Apply Updates. Up-to-Date clusters are a healthy steady state; + # Not-Ready clusters were held back by the readiness gate (e.g. + # state=NeedsAttention / UpdateInProgress / UpdateFailed). + UpToDateCount = "${{ needs.check-readiness.outputs.up_to_date_count }}" + NotReadyCount = "${{ needs.check-readiness.outputs.not_ready_count }}" Succeeded = "${{ steps.apply-updates.outputs.SUCCEEDED }}" Skipped = "${{ steps.apply-updates.outputs.SKIPPED }}" Failed = "${{ steps.apply-updates.outputs.FAILED }}" diff --git a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/assess-update-readiness.yml b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/assess-update-readiness.yml index dbcc2d45..436c2e68 100644 --- a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/assess-update-readiness.yml +++ b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/assess-update-readiness.yml @@ -72,7 +72,7 @@ env: # this to the version actually installed and to the latest on PSGallery, and emits a # ::notice annotation if the YAML appears stale - prompting you to refresh via # Copy-AzLocalPipelineExample -Update. See Automation-Pipeline-Examples/README.md section 5. - GENERATED_AGAINST_MODULE_VERSION: '0.8.77' + GENERATED_AGAINST_MODULE_VERSION: '0.8.78' # Resolution order for the module version pin (leave all unset to install the latest, # which is the default "fix-forward" behaviour): manual workflow_dispatch input > # repository variable 'REQUIRED_MODULE_VERSION' > empty (latest). diff --git a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/authentication-test.yml b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/authentication-test.yml index 15c321e3..731926b4 100644 --- a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/authentication-test.yml +++ b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/authentication-test.yml @@ -79,7 +79,7 @@ env: # this to the version actually installed and to the latest on PSGallery, and emits a # ::notice annotation if the YAML appears stale - prompting you to refresh via # Copy-AzLocalPipelineExample. See Automation-Pipeline-Examples/README.md section 5. - GENERATED_AGAINST_MODULE_VERSION: '0.8.77' + GENERATED_AGAINST_MODULE_VERSION: '0.8.78' # Resolution order for the module version pin (leave all unset to install the latest, # which is the default "fix-forward" behaviour): manual workflow_dispatch input > # repository variable 'REQUIRED_MODULE_VERSION' > empty (latest). diff --git a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/fleet-connectivity-status.yml b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/fleet-connectivity-status.yml index d81284da..9cec88ec 100644 --- a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/fleet-connectivity-status.yml +++ b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/fleet-connectivity-status.yml @@ -116,7 +116,7 @@ env: # PSGallery, and emits a ::notice annotation if the YAML appears stale - # prompting you to refresh via Copy-AzLocalPipelineExample -Update. See # Automation-Pipeline-Examples/README.md section 5. - GENERATED_AGAINST_MODULE_VERSION: '0.8.77' + GENERATED_AGAINST_MODULE_VERSION: '0.8.78' # Resolution order for the module version pin (leave all unset to install # the latest, the default "fix-forward" behaviour): manual workflow_dispatch # input > repository variable 'REQUIRED_MODULE_VERSION' > empty (latest). diff --git a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/fleet-health-status.yml b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/fleet-health-status.yml index 22ebed5b..ea8e14a1 100644 --- a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/fleet-health-status.yml +++ b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/fleet-health-status.yml @@ -116,7 +116,7 @@ env: # this to the version actually installed and to the latest on PSGallery, and emits a # ::notice annotation if the YAML appears stale - prompting you to refresh via # Copy-AzLocalPipelineExample -Update. See Automation-Pipeline-Examples/README.md section 5. - GENERATED_AGAINST_MODULE_VERSION: '0.8.77' + GENERATED_AGAINST_MODULE_VERSION: '0.8.78' # Resolution order for the module version pin (leave all unset to install the latest, # which is the default "fix-forward" behaviour): manual workflow_dispatch input > # repository variable 'REQUIRED_MODULE_VERSION' > empty (latest). diff --git a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/fleet-update-status.yml b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/fleet-update-status.yml index 94613eb1..15a9364a 100644 --- a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/fleet-update-status.yml +++ b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/fleet-update-status.yml @@ -105,7 +105,7 @@ env: # this to the version actually installed and to the latest on PSGallery, and emits a # ::notice annotation if the YAML appears stale - prompting you to refresh via # Copy-AzLocalPipelineExample -Update. See Automation-Pipeline-Examples/README.md section 5. - GENERATED_AGAINST_MODULE_VERSION: '0.8.77' + GENERATED_AGAINST_MODULE_VERSION: '0.8.78' # Resolution order for the module version pin (leave all unset to install the latest, # which is the default "fix-forward" behaviour): manual workflow_dispatch input > # repository variable 'REQUIRED_MODULE_VERSION' > empty (latest). diff --git a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/inventory-clusters.yml b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/inventory-clusters.yml index 001b773d..ed97e7ab 100644 --- a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/inventory-clusters.yml +++ b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/inventory-clusters.yml @@ -48,7 +48,7 @@ env: # this to the version actually installed and to the latest on PSGallery, and emits a # ::notice annotation if the YAML appears stale - prompting you to refresh via # Copy-AzLocalPipelineExample. See Automation-Pipeline-Examples/README.md section 5. - GENERATED_AGAINST_MODULE_VERSION: '0.8.77' + GENERATED_AGAINST_MODULE_VERSION: '0.8.78' # Resolution order for the module version pin (leave all unset to install the latest, # which is the default "fix-forward" behaviour): manual workflow_dispatch input > # repository variable 'REQUIRED_MODULE_VERSION' > empty (latest). diff --git a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/manage-updatering-tags.yml b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/manage-updatering-tags.yml index 98f55e66..ca71e298 100644 --- a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/manage-updatering-tags.yml +++ b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/manage-updatering-tags.yml @@ -65,7 +65,7 @@ env: # this to the version actually installed and to the latest on PSGallery, and emits a # ::notice annotation if the YAML appears stale - prompting you to refresh via # Copy-AzLocalPipelineExample -Update. See Automation-Pipeline-Examples/README.md section 5. - GENERATED_AGAINST_MODULE_VERSION: '0.8.77' + GENERATED_AGAINST_MODULE_VERSION: '0.8.78' # Resolution order for the module version pin (leave all unset to install the latest, # which is the default "fix-forward" behaviour): manual workflow_dispatch input > # repository variable 'REQUIRED_MODULE_VERSION' > empty (latest). diff --git a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/monitor-updates.yml b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/monitor-updates.yml index 2f1d0e4a..610a0fcf 100644 --- a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/monitor-updates.yml +++ b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/monitor-updates.yml @@ -86,7 +86,7 @@ env: # this to the version actually installed and to the latest on PSGallery, and emits a # ::notice annotation if the YAML appears stale - prompting you to refresh via # Copy-AzLocalPipelineExample -Update. See Automation-Pipeline-Examples/README.md section 5. - GENERATED_AGAINST_MODULE_VERSION: '0.8.77' + GENERATED_AGAINST_MODULE_VERSION: '0.8.78' # Resolution order for the module version pin (leave all unset to install the latest, # which is the default "fix-forward" behaviour): manual workflow_dispatch input > # repository variable 'REQUIRED_MODULE_VERSION' > empty (latest). diff --git a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/sideload-updates.yml b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/sideload-updates.yml index d1e71f4b..b280ba75 100644 --- a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/sideload-updates.yml +++ b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/sideload-updates.yml @@ -83,7 +83,7 @@ env: # this to the version actually installed and to the latest on PSGallery, and emits a # ::notice annotation if the YAML appears stale - prompting you to refresh via # Copy-AzLocalPipelineExample -Update. See Automation-Pipeline-Examples/README.md section 5. - GENERATED_AGAINST_MODULE_VERSION: '0.8.77' + GENERATED_AGAINST_MODULE_VERSION: '0.8.78' # Resolution order for the module version pin (leave all unset to install the latest, # which is the default "fix-forward" behaviour): manual workflow_dispatch input > # repository variable 'REQUIRED_MODULE_VERSION' > empty (latest). diff --git a/AzLocal.UpdateManagement/AzLocal.UpdateManagement.psd1 b/AzLocal.UpdateManagement/AzLocal.UpdateManagement.psd1 index 365e66a7..6a8e872a 100644 --- a/AzLocal.UpdateManagement/AzLocal.UpdateManagement.psd1 +++ b/AzLocal.UpdateManagement/AzLocal.UpdateManagement.psd1 @@ -3,7 +3,7 @@ RootModule = 'AzLocal.UpdateManagement.psm1' # Version number of this module. - ModuleVersion = '0.8.77' + ModuleVersion = '0.8.78' # Supported PSEditions CompatiblePSEditions = @('Desktop', 'Core') @@ -309,6 +309,15 @@ # ReleaseNotes of this module ReleaseNotes = @' +## Version 0.8.78 - Patch: pipeline-summary UX polish driven by operator feedback. Three improvements land together: (1) JUnit re-classification - `ScheduleBlocked` / `SideloadedBlocked` / `ExcludedByTag` are designed operator-configured gate-respect outcomes, not failures; they no longer flip Step.07 runs RED via `dorny/test-reporter` and now render as `` (matches the summary `` attr). `HealthCheckBlocked` stays a failure (a Critical health failure blocking an update IS actionable). (2) Apply Updates summary now surfaces the FULL ring picture - the new `Add-AzLocalApplyUpdatesStepSummary -UpToDateCount` / `-NotReadyCount` parameters add `Already Up to Date` and `Not Ready (needs attention before updating)` rows to the Readiness KPI table, so the operator always sees how many clusters are healthy steady state (already patched) vs held back by the readiness gate (state=NeedsAttention / UpdateInProgress / UpdateFailed, pending SBE prereqs, critical health failures). Both apply-updates YAMLs (GitHub Actions + Azure DevOps) wire the upstream `readiness.UpToDateCount` / `readiness.NotReadyCount` outputs through. (3) `actions/download-artifact@v6` -> `@v7` bump in `apply-updates.yml` (GHA) - silences the Node.js 20 deprecation warning that v6 was emitting on every Step.07 run; v7 runs natively on Node 24. No public API change or new exports (still 60). All bundled pipeline templates bump `GENERATED_AGAINST_MODULE_VERSION` from `'0.8.77'` to `'0.8.78'`. + +- **FIX (`Export-ResultsToJUnitXml`)**: `ScheduleBlocked` / `SideloadedBlocked` / `ExcludedByTag` previously emitted `` etc., which made `dorny/test-reporter` flip Step.07 runs RED with `##[error]Failed test were found` on otherwise successful schedule-aware / sideload-gated / tag-excluded runs. They now render as `` and the summary `` / `` attributes agree. `HealthCheckBlocked` deliberately remains in the failure bucket - a Critical health failure blocking an update is a real operational issue the team should action. +- **CHANGE (`Add-AzLocalApplyUpdatesStepSummary`)**: new optional `-UpToDateCount` / `-NotReadyCount` parameters (default `-1` = unknown, row omitted) expand the Readiness KPI table from 2 rows (Total / Ready) to 4 rows (Total / Ready / Already Up to Date / Not Ready). Up-to-Date is a healthy steady state (already fully patched); Not-Ready surfaces the count of clusters correctly held back by the readiness gate so the operator sees them in the apply-updates summary, not only in the no-ready-clusters summary. Backwards-compatible: existing callers that omit the new params see the same 2-row table they always did. +- **CHANGE (`apply-updates.yml` GitHub Actions + Azure DevOps)**: both templates now pipe the upstream check-readiness job/stage's `up_to_date_count` / `not_ready_count` outputs (already emitted since v0.8.74) through to the Generate Summary step. ADO additionally wires two new `variables:` (`upToDateCount` / `notReadyCount`) on the Apply Updates job pulling from `stageDependencies.CheckReadiness.ReadinessCheck.outputs['readiness.UpToDateCount' / '.NotReadyCount']`. +- **CHANGE (`apply-updates.yml` GitHub Actions)**: `actions/download-artifact@v6` -> `@v7` for the readiness-report download step. v6 still ran on Node.js 20 and was emitting `Node.js 20 actions are deprecated` warnings on every Step.07 run; v7 runs natively on Node 24 and silences the warning. v7 (not v8) is the safer choice because v8's breaking change defaults the `digest-mismatch` setting to `error`. +- **TESTS**: existing `Context 'JUnit XML export handles ScheduleBlocked'` and `'JUnit XML export handles SideloadedBlocked'` updated to assert `` instead of `` plus matching summary attrs (`[int]$suite.failures | Should -Be 0`, `[int]$suite.skipped | Should -Be 1`). New `It 'Export-ResultsToJUnitXml renders ExcludedByTag as (v0.8.78)'` regression. New `It 'Has v0.8.78 optional readiness-breakdown parameters UpToDateCount/NotReadyCount'` parameter-shape regression on `Add-AzLocalApplyUpdatesStepSummary`. +- **All bundled pipeline templates** bump `GENERATED_AGAINST_MODULE_VERSION` from `'0.8.77'` to `'0.8.78'`. + ## Version 0.8.77 - Patch: fixes two production strict-mode crashes that surfaced in Step.05 / Step.06 / Step.07 of the bundled apply-updates pipelines. Both bugs share 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`. Fixes use `PSObject.Properties['Prop'] -and $obj.Prop` guards (and `IDictionary.Contains` for tag bags returned by `Invoke-AzRestJson`). No public API change or new exports (still 60). All bundled pipeline templates bump `GENERATED_AGAINST_MODULE_VERSION` from `'0.8.76'` to `'0.8.77'`. - **FIX (`Start-AzLocalClusterUpdate` - Step.07 main entry)**: production emitted `Error processing cluster '': The property 'UpdateStartWindow' cannot be found on this object.` for clusters whose tag bag did not include `UpdateStartWindow` / `UpdateExclusionsWindow`. The old `if ($clusterTags -and $clusterTags.$($script:UpdateStartWindowTagName))` threw BEFORE `-and` short-circuited because bare `.$()` member access on a missing PSCustomObject property is strict-mode-fatal. New guard branches on `$clusterTags -is [System.Collections.IDictionary]` and uses `.Contains()` for hashtables / `PSObject.Properties[...]` for PSCustomObjects. The semantic intent ('absent = any time eligible / no window restriction') is preserved: the outer `if ($windowTagValue -or $exclusionTagValue)` correctly skips the schedule gate when both are `$null`. @@ -316,23 +325,15 @@ - **TESTS**: two new Pester regression contexts (`v0.8.77 strict-mode regression: missing UpdateStartWindow tag` on `Start-AzLocalClusterUpdate`; `v0.8.77 strict-mode regression: missing properties.healthCheckResult` on `Test-AzLocalClusterHealth`) that feed the minimal real-world property-less shapes and assert `Should -Not -Throw` plus correct downstream classification. Both bugs were INVISIBLE to `Get-CmdletErrors` / parse-time analysis - only runtime with the property-less shape triggers the throw. - **All bundled pipeline templates** bump `GENERATED_AGAINST_MODULE_VERSION` from `'0.8.76'` to `'0.8.77'`. -## Version 0.8.76 - Patch: 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 produced `Status: Skipped` with no logs, no annotation, and no actionable feedback. v0.8.76 prepends a `preflight` job (`runs-on: windows-latest`, ~10s, no Azure access) that ALWAYS runs and writes a clear panel to the run step summary. Also broadens the master gate to accept `'true'` / `'True'` / `'TRUE'` / `'1'`. See CHANGELOG for the full v0.8.76 entry. - -## Version 0.8.75 - Patch: removes the duplicate Cycle calendar from the Step.3 apply-updates schedule audit step summary on runs with Recommend-view findings (uncovered/partial cron windows, missing or orphan rings, unparseable crons, or NoWindowTag when a cluster CSV was supplied). Clean fleets were unaffected. New additive `-OmitCycleCalendar` switch on `Test-AzLocalApplyUpdatesScheduleCoverage`; `Export-AzLocalApplyUpdatesScheduleAudit` opts in so a single enriched calendar renders in every scenario. Drift-notice banner now recommends `Update-AzLocalPipelineExample` (marker-aware merge) with the right `-Platform` and a `-Destination` hint per detected host. Three Step.3 step-summary screenshots added. No public API or export-count change (still 60). See CHANGELOG.md for the full per-bullet detail. - -## Version 0.8.74 - Consistent "Up to Date" classification across Step.5 / Step.7 / Step.9 readiness reports, deep-tree fix for the `Progress` column in the Step.7 monitor and the standalone HTML "Recent Update Run History" report, and removal of dead `target="_blank"` portal-link attrs (GH / ADO step-summary sanitisers strip them - replaced with an explicit Ctrl/Cmd/middle-click tip). New shared `Get-AzLocalClusterReadinessStatus` private helper is the single source of truth for the readiness priority cascade. Step.7 / Step.5 swap their binary `Ready?` column for a readable `Status` column, Step.7 adds a `UP_TO_DATE_COUNT` step output, and the "No Clusters Ready" summary now renders an Up-to-Date vs Not-Ready breakdown so an idle apply-updates run is shown as healthy steady state (notice, not warning). New `Get-AzLocalUpdateRunStepStats` private helper walks the nested step tree so leaf-step progress climbs as the install proceeds. All bundled pipeline templates bump `GENERATED_AGAINST_MODULE_VERSION` from `'0.8.73'` to `'0.8.74'`. No public API or export-count change (still 60). See CHANGELOG.md / docs/release-history.md for the full per-bullet detail. +## Version 0.8.76 - Patch: 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 so triggering it without first completing the opt-in setup produces an actionable panel rather than a silent `Status: Skipped`. Also broadens the master gate to accept `'true'` / `'True'` / `'TRUE'` / `'1'`. See CHANGELOG for the full v0.8.76 entry. -## Version 0.8.73 - Cycle calendar: cluster counts folded INLINE into the "Eligible rings" column (Step.3 apply-updates schedule audit). No public API or export-count change (still 60). +## Version 0.8.75 - Patch: removes the duplicate Cycle calendar from the Step.3 apply-updates schedule audit step summary on Recommend-view runs. New additive `-OmitCycleCalendar` switch; drift-notice banner now recommends `Update-AzLocalPipelineExample`. No public API or export-count change (still 60). See CHANGELOG.md for the full v0.8.75 entry. -- **CHANGE**: `Get-AzLocalApplyUpdatesScheduleCycleCalendar` no longer renders a separate "Clusters in ring(s)" column when `-ClusterRingCounts` is supplied. Instead the "Eligible rings" header is relabelled "Eligible rings (cluster count)" and each ring token carries its count inline, e.g. `` `Prod` (9), `Canary` (3) ``. Dead days still render `_(none - dead day)_`. The per-ring projection table keeps its separate "Cluster count" column. -- **FIX**: `Export-AzLocalApplyUpdatesScheduleAudit` (the Step.3 pipeline render path) now builds a ring -> tagged-cluster-count map from the cluster CSV and forwards it as `-ClusterRingCounts`. Previously this caller never passed the parameter, so the cluster counts never appeared in the rendered Step.3 cycle calendar even though the cmdlet supported them. -- **All bundled pipeline templates** bump `GENERATED_AGAINST_MODULE_VERSION` from `'0.8.72'` to `'0.8.73'`. +## Version 0.8.74 - Consistent "Up to Date" classification across Step.5 / Step.7 / Step.9 readiness reports; deep-tree fix for the `Progress` column in the Step.7 monitor and standalone HTML "Recent Update Run History" report; new shared `Get-AzLocalClusterReadinessStatus` private helper as single source of truth for the readiness cascade. Step.7 / Step.5 add a readable `Status` column and Step.7 adds a `UP_TO_DATE_COUNT` step output. No public API or export-count change (still 60). See CHANGELOG.md / docs/release-history.md for the full per-bullet detail. -## Version 0.8.72 - Patch: pipeline-template polish only. Schedule-file author guidance moved OUTSIDE the apply-updates.yml customise marker so it refreshes on existing consumers; single-digit Step.N display names zero-padded (Step.0 -> Step.00 ... Step.9 -> Step.09) so the GitHub Actions sidebar / Azure DevOps list sort in execution order. No public API or export-count change (still 60). +## Version 0.8.73 - Cycle calendar: cluster counts folded INLINE into the "Eligible rings" column (Step.3 apply-updates schedule audit). No public API or export-count change (still 60). See CHANGELOG for the full v0.8.73 entry. -- **FIX**: `apply-updates.yml` (GitHub Actions + Azure DevOps) - the schedule-file author guidance comments were trapped INSIDE the `# BEGIN/END-AZLOCAL-CUSTOMIZE:schedule-triggers` block, so corrections could never reach an already-deployed consumer. All author guidance is now ABOVE the marker; the marker body holds only the trigger directive. -- **CHANGE (display only)**: single-digit pipeline step numbers zero-padded to two digits in all display names / titles (`Step.0` -> `Step.00` ... `Step.9` -> `Step.09`; `Step.10` unchanged) so the GitHub Actions sidebar and Azure DevOps list sort in execution order. Functional identifiers (artifact names) are unchanged. -- **All bundled pipeline templates** bump `GENERATED_AGAINST_MODULE_VERSION` from `'0.8.71'` to `'0.8.72'`. +## Version 0.8.72 - Patch: pipeline-template polish only. Schedule-file author guidance moved OUTSIDE the apply-updates.yml customise marker so it refreshes on existing consumers; single-digit Step.N display names zero-padded (Step.0 -> Step.00 ... Step.9 -> Step.09) so the GitHub Actions sidebar / Azure DevOps list sort in execution order. No public API or export-count change (still 60). See CHANGELOG for the full v0.8.72 entry. ## Version 0.8.71 - Patch: production strict-mode crash fix in `Export-ResultsToJUnitXml` (`UpdateStarted` success row lacked `CurrentState`/`Progress`); GitHub Actions `sideload-updates.yml` schedule-path default corrected `./.github/` -> `./config/`; stale `Step.N_*.yml` filename references in pipeline doc strings de-numbered. No public API or export-count change (still 60). See CHANGELOG for the full v0.8.71 entry. diff --git a/AzLocal.UpdateManagement/AzLocal.UpdateManagement.psm1 b/AzLocal.UpdateManagement/AzLocal.UpdateManagement.psm1 index 82e6eba2..fd3ed94d 100644 --- a/AzLocal.UpdateManagement/AzLocal.UpdateManagement.psm1 +++ b/AzLocal.UpdateManagement/AzLocal.UpdateManagement.psm1 @@ -151,7 +151,7 @@ Set-StrictMode -Version 1.0 # bumps to one but not the other are caught before release. Two consumers: # - Start-AzLocalClusterUpdate emits this in the run log header. # - Get-AzLocalFleetStatusData stamps it into exported fleet-state JSON. -$script:ModuleVersion = '0.8.77' +$script:ModuleVersion = '0.8.78' $script:DefaultApiVersion = '2025-10-01' $script:DefaultLogFolder = Join-Path -Path $env:ProgramData -ChildPath 'AzLocal.UpdateManagement' diff --git a/AzLocal.UpdateManagement/CHANGELOG.md b/AzLocal.UpdateManagement/CHANGELOG.md index c7cd1a5a..1d735e73 100644 --- a/AzLocal.UpdateManagement/CHANGELOG.md +++ b/AzLocal.UpdateManagement/CHANGELOG.md @@ -5,6 +5,39 @@ All notable changes to the AzLocal.UpdateManagement module (renamed from AzStack The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.8.78] - 2026-06-15 + +Patch release. Pipeline-summary UX polish driven by operator feedback on the bundled apply-updates pipeline (Step.07). Three improvements land together, plus the routine `actions/download-artifact` deprecation bump: + +1. **JUnit re-classification of designed gate-respect outcomes.** `ScheduleBlocked` (cluster honouring its `UpdateStartWindow`), `SideloadedBlocked` (cluster honouring `UpdateSideloaded=False`), and `ExcludedByTag` (cluster honouring `AzureLocalManagement.UpdateExcluded=true`) are operator-configured opt-outs, not failures. They were previously rendered as `` rows by `Export-ResultsToJUnitXml`, which made `dorny/test-reporter` flip Step.07 runs RED with `##[error]Failed test were found` on otherwise-successful schedule-aware / sideload-gated / tag-excluded runs. They now render as `` and the summary `` / `` attributes agree. `HealthCheckBlocked` deliberately remains in the failure bucket - a Critical health failure blocking an update IS a real operational issue the team should action. +2. **Apply Updates summary now shows the FULL ring picture.** Previously the Readiness KPI table in the Apply Updates step summary showed only Total Clusters + Ready for Update, meaning the operator had no idea how many clusters were already fully patched (healthy steady state) or held back by the readiness gate (`state=NeedsAttention` / `UpdateInProgress` / `UpdateFailed` / pending SBE prerequisites / Critical health failure). The new optional `-UpToDateCount` / `-NotReadyCount` parameters on `Add-AzLocalApplyUpdatesStepSummary` add `Already Up to Date` and `Not Ready (needs attention before updating)` rows. Both apply-updates YAMLs (GitHub Actions + Azure DevOps) wire the upstream `readiness.UpToDateCount` / `readiness.NotReadyCount` outputs (already emitted by the check-readiness job/stage since v0.8.74) through to the summary task. +3. **`actions/download-artifact@v6` -> `@v7` in `apply-updates.yml` (GitHub Actions).** v6 still ran on Node.js 20 by default and was emitting `Node.js 20 actions are deprecated. Please update the following actions to use Node.js 24` warnings on every Step.07 run. `@v7` (Dec 2025 release) runs natively on Node 24 and silences the warning. **`@v7` is chosen deliberately over `@v8`**: v8 introduced a breaking change defaulting the `digest-mismatch` setting to `error`, which is more disruptive than the v6 -> v7 upgrade benefit. + +No public API change or new exports (still 60). All bundled pipeline templates bump `GENERATED_AGAINST_MODULE_VERSION` from `'0.8.77'` to `'0.8.78'`. + +### Fixed + +- **`Export-ResultsToJUnitXml` - JUnit re-classification of designed gate-respect outcomes.** Moved `ScheduleBlocked`, `SideloadedBlocked`, and `ExcludedByTag` from the `` bucket to the `` bucket BOTH in the summary `` / `` attributes AND in the per-testcase switch. `HealthCheckBlocked` deliberately retained in the failure bucket. `ExcludedByTag` was previously not explicitly classified at all - it fell through to the `` default branch, which made it look like a successful test in `dorny/test-reporter` (now correctly classified as skipped). + +### Added + +- **`Add-AzLocalApplyUpdatesStepSummary` - optional `-UpToDateCount` and `-NotReadyCount` parameters.** Default `-1` (unknown) so the corresponding Readiness KPI rows are quietly omitted when callers do not supply them - existing consumers that have not regenerated their YAMLs see the same 2-row Readiness table they always did. When supplied (>= 0), the Readiness KPI table expands from 2 rows to 4 rows: `Total Clusters` / `Ready for Update` / `Already Up to Date` / `Not Ready (needs attention before updating)`. Up-to-Date is surfaced as a healthy steady state; Not-Ready surfaces clusters correctly held back by the readiness gate (state=NeedsAttention / UpdateInProgress / UpdateFailed, pending SBE prerequisites, critical health failures). + +### Changed + +- **`apply-updates.yml` (GitHub Actions)**: `actions/download-artifact@v6` -> `@v7` for the readiness-report download step (silences Node.js 20 deprecation warning). The `Summary` step now forwards `UpToDateCount` and `NotReadyCount` to `Add-AzLocalApplyUpdatesStepSummary` from `${{ needs.check-readiness.outputs.up_to_date_count }}` / `${{ needs.check-readiness.outputs.not_ready_count }}` (the check-readiness job has emitted these outputs since v0.8.74). +- **`apply-updates.yml` (Azure DevOps)**: the `ApplyClusterUpdates` job adds two new `variables:` (`upToDateCount` / `notReadyCount`) pulling from `stageDependencies.CheckReadiness.ReadinessCheck.outputs['readiness.UpToDateCount']` / `'.NotReadyCount'`. The `Generate Summary` task forwards them to `Add-AzLocalApplyUpdatesStepSummary`. + +### Tests + +- `Context 'JUnit XML export handles ScheduleBlocked'` and `Context 'JUnit XML export handles SideloadedBlocked (v0.7.1, reclassified v0.8.78)'` updated to assert the per-testcase element is `` (not ``) AND the summary `` attributes agree (`failures=0`, `skipped=1`). +- New `It 'Export-ResultsToJUnitXml renders ExcludedByTag as (v0.8.78)'` covers the third designed-skip status, asserting both per-testcase and summary-attr correctness. +- New `It 'Has v0.8.78 optional readiness-breakdown parameters UpToDateCount/NotReadyCount'` parameter-shape regression on `Add-AzLocalApplyUpdatesStepSummary` (in the existing `Thin-YAML Step.6: Add-AzLocalApplyUpdatesStepSummary` describe). + +### Pipeline templates + +- All bundled pipeline templates bump `GENERATED_AGAINST_MODULE_VERSION` from `'0.8.77'` to `'0.8.78'`. Bodies are unchanged except for the GitHub Actions `apply-updates.yml` (download-artifact bump + UpToDateCount/NotReadyCount wiring) and the Azure DevOps `apply-updates.yml` (UpToDateCount/NotReadyCount wiring). + ## [0.8.77] - 2026-06-14 Patch release. Fixes two production strict-mode crashes that surfaced in Step.05 / Step.06 / Step.07 of the bundled apply-updates pipelines. Both bugs share 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`. Fixes use the `$obj.PSObject.Properties['Prop'] -and $obj.Prop` guard idiom (and `IDictionary.Contains()` for tag bags returned by `Invoke-AzRestJson`). No public API change or new exports (still 60). All bundled pipeline templates bump `GENERATED_AGAINST_MODULE_VERSION` from `'0.8.76'` to `'0.8.77'`. diff --git a/AzLocal.UpdateManagement/Private/Export-ResultsToJUnitXml.ps1 b/AzLocal.UpdateManagement/Private/Export-ResultsToJUnitXml.ps1 index add228ea..a71433f0 100644 --- a/AzLocal.UpdateManagement/Private/Export-ResultsToJUnitXml.ps1 +++ b/AzLocal.UpdateManagement/Private/Export-ResultsToJUnitXml.ps1 @@ -45,8 +45,17 @@ function Export-ResultsToJUnitXml { # while UpdateNotFound rows had . Tools like dorny/test-reporter use the # summary attributes for the headline numbers, so the discrepancy produced # misleading "all green" CI summaries. - $failures = @($Results | Where-Object { $_.Status -in @("Failed", "Error", "HealthCheckBlocked", "ScheduleBlocked", "SideloadedBlocked") }).Count - $skipped = @($Results | Where-Object { $_.Status -in @("Skipped", "NotReady", "NotConnected", "NoUpdatesAvailable", "NoReadyUpdates") }).Count + # v0.8.78: ScheduleBlocked / SideloadedBlocked / ExcludedByTag are DESIGNED + # no-ops (operator-configured gates honoured by Start-AzLocalClusterUpdate), + # not failures. Previously they emitted rows, which made dorny/test- + # reporter flip Step.7 runs RED with '##[error]Failed test were found' even + # though the cluster simply respected its UpdateStartWindow / UpdateSideloaded / + # UpdateExcluded tag. They now render as alongside NoUpdatesAvailable + # so legitimate gate-respect outcomes do not look like apply failures. + # HealthCheckBlocked remains in the failure bucket - a Critical health check + # blocking an update IS an operational issue the team should action. + $failures = @($Results | Where-Object { $_.Status -in @("Failed", "Error", "HealthCheckBlocked") }).Count + $skipped = @($Results | Where-Object { $_.Status -in @("Skipped", "NotReady", "NotConnected", "NoUpdatesAvailable", "NoReadyUpdates", "ScheduleBlocked", "SideloadedBlocked", "ExcludedByTag") }).Count $errors = @($Results | Where-Object { $_.Status -in @("NotFound", "UpdateNotFound") }).Count $timestamp = Get-Date -Format "yyyy-MM-ddTHH:mm:ss" @@ -110,9 +119,13 @@ function Export-ResultsToJUnitXml { [void]$xmlBuilder.AppendLine(" ") switch ($result.Status) { - { $_ -in @("Failed", "Error", "HealthCheckBlocked", "ScheduleBlocked", "SideloadedBlocked") } { + # v0.8.78: ScheduleBlocked / SideloadedBlocked / ExcludedByTag moved to + # the branch below (designed gate-respect, not failure). + # HealthCheckBlocked remains here because a Critical health failure + # blocking an update IS something operators need to action. + { $_ -in @("Failed", "Error", "HealthCheckBlocked") } { $message = ConvertTo-XmlSafeString ($result.Message) - $errorType = if ($result.Status -eq "Error") { "Error" } elseif ($result.Status -eq "HealthCheckBlocked") { "HealthCheckBlocked" } elseif ($result.Status -eq "ScheduleBlocked") { "ScheduleBlocked" } elseif ($result.Status -eq "SideloadedBlocked") { "SideloadedBlocked" } else { "AssertionError" } + $errorType = if ($result.Status -eq "Error") { "Error" } elseif ($result.Status -eq "HealthCheckBlocked") { "HealthCheckBlocked" } else { "AssertionError" } [void]$xmlBuilder.AppendLine(" ") [void]$xmlBuilder.AppendLine("Cluster: $clusterName") [void]$xmlBuilder.AppendLine("Status: $($result.Status)") @@ -153,10 +166,12 @@ function Export-ResultsToJUnitXml { [void]$xmlBuilder.AppendLine("Message: $message") [void]$xmlBuilder.AppendLine(" ") } - { $_ -in @("Skipped", "NotReady", "NotConnected", "NoUpdatesAvailable", "NoReadyUpdates") } { + { $_ -in @("Skipped", "NotReady", "NotConnected", "NoUpdatesAvailable", "NoReadyUpdates", "ScheduleBlocked", "SideloadedBlocked", "ExcludedByTag") } { # v0.7.62: previously only literal "Skipped" rendered as ; the # other "did not apply, but not a failure" Status values fell through to # , producing misleading "all green" CI summaries. + # v0.8.78: ScheduleBlocked / SideloadedBlocked / ExcludedByTag added here - + # they are DESIGNED operator-configured gate-respect outcomes, not failures. $message = ConvertTo-XmlSafeString ($result.Message) [void]$xmlBuilder.AppendLine(" ") } diff --git a/AzLocal.UpdateManagement/Public/Add-AzLocalApplyUpdatesStepSummary.ps1 b/AzLocal.UpdateManagement/Public/Add-AzLocalApplyUpdatesStepSummary.ps1 index 18ff0604..271be687 100644 --- a/AzLocal.UpdateManagement/Public/Add-AzLocalApplyUpdatesStepSummary.ps1 +++ b/AzLocal.UpdateManagement/Public/Add-AzLocalApplyUpdatesStepSummary.ps1 @@ -36,6 +36,17 @@ function Add-AzLocalApplyUpdatesStepSummary { display, to int for KPI checks). .PARAMETER ReadyCount Readiness ready count. + .PARAMETER UpToDateCount + Readiness up-to-date count (clusters already fully patched - no action + needed). Optional; defaults to -1 ("unknown") in which case the + Readiness table omits the row. (v0.8.78) + .PARAMETER NotReadyCount + Readiness not-ready count (clusters held back by a previously-failed + run, an in-progress run, pending SBE prerequisites or a critical + health-check failure - typically clusters in the 'NeedsAttention', + 'UpdateInProgress', 'UpdateFailed' or 'HasPrerequisite' states). + Optional; defaults to -1 ("unknown") in which case the Readiness + table omits the row. (v0.8.78) .PARAMETER Succeeded Apply succeeded count. .PARAMETER Skipped @@ -83,6 +94,11 @@ function Add-AzLocalApplyUpdatesStepSummary { [Parameter(Mandatory = $false)] [object]$TotalCount = 0, [Parameter(Mandatory = $false)] [object]$ReadyCount = 0, + # v0.8.78: optional readiness-gate breakdown so operators see the full + # ring picture (Up to Date / Not Ready) alongside Ready, not just the + # apply-buckets. -1 = unknown -> row omitted. + [Parameter(Mandatory = $false)] [object]$UpToDateCount = -1, + [Parameter(Mandatory = $false)] [object]$NotReadyCount = -1, [Parameter(Mandatory = $false)] [object]$Succeeded = 0, [Parameter(Mandatory = $false)] [object]$Skipped = 0, [Parameter(Mandatory = $false)] [object]$Failed = 0, @@ -132,6 +148,17 @@ function Add-AzLocalApplyUpdatesStepSummary { # Coerce numeric inputs. $totalInt = [int]([string]$TotalCount) $readyInt = [int]([string]$ReadyCount) + # v0.8.78: tolerate empty strings (unresolved pipeline macros) by parsing + # to -1 ("unknown") so the Readiness rows are quietly omitted instead of + # throwing or rendering '0' for callers that haven't been updated yet. + $parseOptional = { + param($value) + $parsed = 0 + if ([int]::TryParse([string]$value, [ref]$parsed)) { return $parsed } + return -1 + } + $upToDateInt = & $parseOptional $UpToDateCount + $notReadyInt = & $parseOptional $NotReadyCount $succeededInt = [int]([string]$Succeeded) $skippedInt = [int]([string]$Skipped) $failedInt = [int]([string]$Failed) @@ -160,6 +187,20 @@ function Add-AzLocalApplyUpdatesStepSummary { [void]$sb.AppendLine('|--------|-------|') [void]$sb.AppendLine("| Total Clusters | $totalInt |") [void]$sb.AppendLine("| Ready for Update | $readyInt |") + # v0.8.78: surface Up to Date and Not Ready so the operator sees the FULL + # ring picture in the apply-updates summary - not just the clusters that + # entered Apply Updates. Up-to-Date is a healthy steady state (already + # fully patched, nothing to apply). Not-Ready includes clusters held back + # by a previously-failed run (state=NeedsAttention / UpdateFailed), an + # in-progress run (UpdateInProgress), pending SBE prerequisites, or a + # critical health failure - those clusters were correctly skipped by the + # readiness gate and Start-AzLocalClusterUpdate's Step 3 state check. + if ($upToDateInt -ge 0) { + [void]$sb.AppendLine("| Already Up to Date | $upToDateInt |") + } + if ($notReadyInt -ge 0) { + [void]$sb.AppendLine("| Not Ready (needs attention before updating) | $notReadyInt |") + } [void]$sb.AppendLine() [void]$sb.AppendLine("$h3 Results") diff --git a/AzLocal.UpdateManagement/README.md b/AzLocal.UpdateManagement/README.md index 5339cb2c..5c880ffb 100644 --- a/AzLocal.UpdateManagement/README.md +++ b/AzLocal.UpdateManagement/README.md @@ -2,7 +2,7 @@ > ⚠️ **Disclaimer**: This module is **NOT** a Microsoft supported service offering or product. It is provided as example code only, with no warranty or official support. Refer to the [MIT license](https://github.com/NeilBird/Azure-Local/blob/main/LICENSE) for further information. -**Latest Version:** v0.8.77 - [Published in PowerShell Gallery](https://www.powershellgallery.com/packages/AzLocal.UpdateManagement/0.8.77) +**Latest Version:** v0.8.78 - [Published in PowerShell Gallery](https://www.powershellgallery.com/packages/AzLocal.UpdateManagement/0.8.78) > 📢 **Renamed in v0.7.3**: this module was previously published as `AzStackHci.ManageUpdates`. The new module name aligns with the Azure Local product name (_Microsoft retired the *Azure Stack HCI* brand in late 2024_). The module GUID is preserved across the rename. If you have the old name installed, run: > @@ -23,7 +23,7 @@ Azure Local REST API specification (includes update management endpoints): https **This README (overview + most-recent release notes):** - [Where to Start](#where-to-start) -- [What's New in v0.8.77](#whats-new-in-v0877) +- [What's New in v0.8.78](#whats-new-in-v0878) - [Files](#files) - [Prerequisites](#prerequisites) - [RBAC Requirements](#rbac-requirements) (summary; full reference in [docs/rbac.md](docs/rbac.md)) @@ -86,17 +86,19 @@ If you are new to this module, work through these in order from a regular PowerS > Most CI/CD pipelines in [Automation-Pipeline-Examples/](Automation-Pipeline-Examples/) are direct implementations of one of these workflows. Start there if you want a copy-pasteable end-to-end pipeline. -## What's New in v0.8.77 +## What's New in v0.8.78 -**Patch release. Fixes two production strict-mode crashes** that surfaced in Step.05 / Step.06 / Step.07 of the bundled apply-updates pipelines. Both bugs share 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`. Fixes use the `$obj.PSObject.Properties['Prop'] -and $obj.Prop` guard idiom (and `IDictionary.Contains()` for tag bags returned by `Invoke-AzRestJson`). No public API change or new exports (still 60). All bundled pipeline templates bump `GENERATED_AGAINST_MODULE_VERSION` from `0.8.76` to `0.8.77`. +**Patch release. Pipeline-summary UX polish** driven by operator feedback on the bundled apply-updates pipeline (Step.07). Three improvements land together: -1. **Fixed (`Start-AzLocalClusterUpdate`, Step.07 main entry)**: production emitted `Error processing cluster '': The property 'UpdateStartWindow' cannot be found on this object.` for clusters whose tag bag did not include `UpdateStartWindow` / `UpdateExclusionsWindow`. The old `if ($clusterTags -and $clusterTags.$($script:UpdateStartWindowTagName))` threw **before** `-and` short-circuited because bare `.$()` member access on a missing `PSCustomObject` property is strict-mode-fatal. New code branches on `$clusterTags -is [System.Collections.IDictionary]` and uses `.Contains()` for hashtables / `PSObject.Properties[...]` for `PSCustomObject` tag bags. The semantic intent ("absent = any time eligible / no window restriction") is preserved: the outer `if ($windowTagValue -or $exclusionTagValue)` correctly skips the schedule gate when both are `$null`. -2. **Fixed (`Test-AzLocalClusterHealth`, Step.05/06 readiness gate)**: production emitted `Checking: ... Error: The property 'healthCheckResult' cannot be found on this object.` for clusters whose ARM update summary genuinely had no `healthCheckResult` field (typical for clusters not yet probed). The `catch` block then flagged the cluster `HealthState=Error / Passed=$false`, and the readiness gate falsely treated it as **blocked**, poisoning Step.5 / Step.7 / Step.9 readiness output for affected clusters. The same bare-access pattern was present in `Get-HealthCheckFailureSummary` (Private helper) and `Get-AzLocalFleetStatusData` (Public, `-IncludeHealthDetails`). All three sites now use the `PSObject.Properties[...]` guard; `Test-AzLocalClusterHealth` correctly classifies such clusters as `HealthState="No Data" / Passed=$true` and continues. -3. **Tests**: two new Pester regression contexts feed the minimal real-world property-less shapes and assert `Should -Not -Throw` plus the correct downstream classification. Both bugs were invisible to parse-time analysis - only runtime against the property-less shape triggers the strict-mode throw. +1. **JUnit re-classification of designed gate-respect outcomes.** `ScheduleBlocked` (cluster honouring its `UpdateStartWindow`), `SideloadedBlocked` (cluster honouring `UpdateSideloaded=False`), and `ExcludedByTag` (cluster honouring `AzureLocalManagement.UpdateExcluded=true`) are operator-configured opt-outs, not failures. They were previously rendered as `` rows by `Export-ResultsToJUnitXml`, which made `dorny/test-reporter` flip Step.07 runs **red** with `##[error]Failed test were found` on otherwise-successful schedule-aware / sideload-gated / tag-excluded runs. They now render as `` and the summary `` / `` attributes agree. `HealthCheckBlocked` deliberately remains in the failure bucket - a Critical health failure blocking an update IS a real operational issue the team should action. +2. **Apply Updates summary now shows the FULL ring picture.** Previously the Readiness KPI table in the Apply Updates step summary showed only Total Clusters + Ready for Update, meaning the operator had no idea how many clusters were already fully patched (healthy steady state) or held back by the readiness gate (`state=NeedsAttention` / `UpdateInProgress` / `UpdateFailed` / pending SBE prerequisites / Critical health failure). The new optional `-UpToDateCount` / `-NotReadyCount` parameters on `Add-AzLocalApplyUpdatesStepSummary` add `Already Up to Date` and `Not Ready (needs attention before updating)` rows. Both apply-updates YAMLs (GitHub Actions + Azure DevOps) wire the upstream `readiness.UpToDateCount` / `readiness.NotReadyCount` outputs (already emitted by the check-readiness job/stage since v0.8.74) through to the summary task. +3. **`actions/download-artifact@v6` -> `@v7` in `apply-updates.yml` (GitHub Actions).** v6 still ran on Node.js 20 and was emitting `Node.js 20 actions are deprecated. Please update the following actions to use Node.js 24` warnings on every Step.07 run. `@v7` (Dec 2025 release) runs natively on Node 24 and silences the warning. `@v7` is chosen deliberately over `@v8`: v8 introduced a breaking change defaulting the `digest-mismatch` setting to `error`, which is more disruptive than the v6 -> v7 upgrade benefit. -`GENERATED_AGAINST_MODULE_VERSION` bumped from `0.8.76` to `0.8.77` across all bundled pipeline templates. +Readiness gate verified robust against the operator question "do we show some clusters as Ready when they have a previously-failed in-progress run?" - the answer is **no**. `Get-AzLocalClusterUpdateReadiness` and `Start-AzLocalClusterUpdate` (Step 3) both predicate Ready on `updateSummary.properties.state -in @('UpdateAvailable','Ready','ReadyToInstall')`. Clusters in `NeedsAttention` / `UpdateInProgress` / `UpdateFailed` / `PreparationFailed` correctly land in Not-Ready and are surfaced by the Cluster Readiness table's `Status` column (added in v0.8.74). -See [CHANGELOG.md](CHANGELOG.md#0877---2026-06-14) for the full v0.8.77 entry. See [`What's New in v0.8.76`](docs/release-history.md#whats-new-in-v0876) in the Release History for the previous release. +`GENERATED_AGAINST_MODULE_VERSION` bumped from `0.8.77` to `0.8.78` across all bundled pipeline templates. + +See [CHANGELOG.md](CHANGELOG.md#0878---2026-06-15) for the full v0.8.78 entry. See [`What's New in v0.8.77`](docs/release-history.md#whats-new-in-v0877) in the Release History for the previous release. ## Files @@ -575,7 +577,11 @@ This code is provided as-is for educational and reference purposes. The full What's-New history (v0.7.81 and earlier) has moved to [docs/release-history.md](docs/release-history.md). -The most recent release notes for **v0.8.77** stay above under [`What's New in v0.8.77`](#whats-new-in-v0877). +The most recent release notes for **v0.8.78** stay above under [`What's New in v0.8.78`](#whats-new-in-v0878). + +### What's New in v0.8.77 + +**Patch release. Fixes two production strict-mode crashes** that surfaced in Step.05 / Step.06 / Step.07 of the bundled apply-updates pipelines. Both bugs share 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`. Fixes use the `$obj.PSObject.Properties['Prop'] -and $obj.Prop` guard idiom (and `IDictionary.Contains()` for tag bags returned by `Invoke-AzRestJson`). No public API change or new exports (still 60). `GENERATED_AGAINST_MODULE_VERSION` bumped from `0.8.76` to `0.8.77` across all bundled pipeline templates. See [CHANGELOG.md](CHANGELOG.md#0877---2026-06-14) for the full per-bullet detail and [docs/release-history.md](docs/release-history.md#whats-new-in-v0877) for the archived entry. ### What's New in v0.8.76 diff --git a/AzLocal.UpdateManagement/Tests/AzLocal.UpdateManagement.Tests.ps1 b/AzLocal.UpdateManagement/Tests/AzLocal.UpdateManagement.Tests.ps1 index 198a98fc..c8d9b4ec 100644 --- a/AzLocal.UpdateManagement/Tests/AzLocal.UpdateManagement.Tests.ps1 +++ b/AzLocal.UpdateManagement/Tests/AzLocal.UpdateManagement.Tests.ps1 @@ -34,8 +34,8 @@ Describe 'Module: AzLocal.UpdateManagement' { $script:ModuleInfo | Should -Not -BeNullOrEmpty } - It 'Should have version 0.8.77' { - $script:ModuleInfo.Version | Should -Be '0.8.77' + It 'Should have version 0.8.78' { + $script:ModuleInfo.Version | Should -Be '0.8.78' } It 'Module version constants are in sync between .psm1 and .psd1' { @@ -3014,7 +3014,12 @@ Describe 'Integration: Start-AzLocalClusterUpdate Schedule Status' { } Context 'JUnit XML export handles ScheduleBlocked' { - It 'Export-ResultsToJUnitXml should handle ScheduleBlocked result' { + It 'Export-ResultsToJUnitXml renders ScheduleBlocked as (v0.8.78)' { + # v0.8.78: ScheduleBlocked is a DESIGNED operator-configured gate-respect + # outcome (UpdateStartWindow honoured), not a failure. Previously rendered + # as , which made dorny/test-reporter + # flip Step.7 runs RED with '##[error]Failed test were found' on otherwise + # successful schedule-aware runs. Must now render as . $testResult = [PSCustomObject]@{ ClusterName = 'test-cluster' Status = 'ScheduleBlocked' @@ -3033,11 +3038,17 @@ Describe 'Integration: Start-AzLocalClusterUpdate Schedule Status' { $outputPath | Should -Exist $xml = [xml](Get-Content $outputPath -Raw) + # Per-testcase element must be , not . $testCase = $xml.SelectSingleNode('//testcase') $testCase | Should -Not -BeNullOrEmpty - $failure = $testCase.SelectSingleNode('failure') - $failure | Should -Not -BeNullOrEmpty - $failure.type | Should -Be 'ScheduleBlocked' + $testCase.SelectSingleNode('failure') | Should -BeNullOrEmpty + $skipped = $testCase.SelectSingleNode('skipped') + $skipped | Should -Not -BeNullOrEmpty + $skipped.message | Should -Match 'Outside maintenance window' + # Summary attrs must agree: 0 failures, 1 skipped. + $suite = $xml.SelectSingleNode('//testsuite') + [int]$suite.failures | Should -Be 0 + [int]$suite.skipped | Should -Be 1 } finally { if (Test-Path $outputPath) { Remove-Item $outputPath -Force } @@ -3045,8 +3056,11 @@ Describe 'Integration: Start-AzLocalClusterUpdate Schedule Status' { } } - Context 'JUnit XML export handles SideloadedBlocked (v0.7.1)' { - It 'Export-ResultsToJUnitXml should handle SideloadedBlocked result' { + Context 'JUnit XML export handles SideloadedBlocked (v0.7.1, reclassified v0.8.78)' { + It 'Export-ResultsToJUnitXml renders SideloadedBlocked as (v0.8.78)' { + # v0.8.78: SideloadedBlocked is a DESIGNED operator-configured gate-respect + # outcome (UpdateSideloaded=False honoured), not a failure. Same fix as + # ScheduleBlocked above. $testResult = [PSCustomObject]@{ ClusterName = 'test-cluster' Status = 'SideloadedBlocked' @@ -3067,9 +3081,49 @@ Describe 'Integration: Start-AzLocalClusterUpdate Schedule Status' { $xml = [xml](Get-Content $outputPath -Raw) $testCase = $xml.SelectSingleNode('//testcase') $testCase | Should -Not -BeNullOrEmpty - $failure = $testCase.SelectSingleNode('failure') - $failure | Should -Not -BeNullOrEmpty - $failure.type | Should -Be 'SideloadedBlocked' + $testCase.SelectSingleNode('failure') | Should -BeNullOrEmpty + $skipped = $testCase.SelectSingleNode('skipped') + $skipped | Should -Not -BeNullOrEmpty + $skipped.message | Should -Match 'UpdateSideloaded' + $suite = $xml.SelectSingleNode('//testsuite') + [int]$suite.failures | Should -Be 0 + [int]$suite.skipped | Should -Be 1 + } + finally { + if (Test-Path $outputPath) { Remove-Item $outputPath -Force } + } + } + + It 'Export-ResultsToJUnitXml renders ExcludedByTag as (v0.8.78)' { + # v0.8.78: ExcludedByTag (AzureLocalManagement.UpdateExcluded=true) is a + # DESIGNED opt-out, not a failure. Previously fell through to ; + # now explicitly classified as . + $testResult = [PSCustomObject]@{ + ClusterName = 'test-cluster' + Status = 'ExcludedByTag' + Message = 'Cluster excluded by AzureLocalManagement.UpdateExcluded=true' + UpdateName = $null + StartTime = Get-Date + EndTime = Get-Date + Duration = '00:00:01' + } + $outputPath = Join-Path $env:TEMP "pester-junit-excluded-test-$([Guid]::NewGuid()).xml" + try { + & (Get-Module 'AzLocal.UpdateManagement') { + param($results, $path) + Export-ResultsToJUnitXml -Results $results -OutputPath $path -TestSuiteName 'Test' -OperationType 'StartUpdate' + } @($testResult) $outputPath + + $outputPath | Should -Exist + $xml = [xml](Get-Content $outputPath -Raw) + $testCase = $xml.SelectSingleNode('//testcase') + $testCase | Should -Not -BeNullOrEmpty + $testCase.SelectSingleNode('failure') | Should -BeNullOrEmpty + $skipped = $testCase.SelectSingleNode('skipped') + $skipped | Should -Not -BeNullOrEmpty + $suite = $xml.SelectSingleNode('//testsuite') + [int]$suite.failures | Should -Be 0 + [int]$suite.skipped | Should -Be 1 } finally { if (Test-Path $outputPath) { Remove-Item $outputPath -Force } @@ -17365,6 +17419,15 @@ Describe 'Thin-YAML Step.6: Add-AzLocalApplyUpdatesStepSummary' { $script:S6CmdS.Parameters.Keys | Should -Contain $n -Because "Add-AzLocalApplyUpdatesStepSummary must expose $n" } } + It 'Has v0.8.78 optional readiness-breakdown parameters UpToDateCount/NotReadyCount' { + # v0.8.78: surface the readiness gate's Up-to-Date / Not-Ready counts so the + # apply-updates summary's Readiness KPI reflects the full ring picture, not + # only the clusters that entered Apply Updates. Both default to -1 (unknown) + # so callers that haven't been updated yet keep working unchanged. + foreach ($n in 'UpToDateCount','NotReadyCount') { + $script:S6CmdS.Parameters.Keys | Should -Contain $n -Because "Add-AzLocalApplyUpdatesStepSummary must expose $n (v0.8.78)" + } + } It 'Has parameter ApplyResultsJsonPath' { $script:S6CmdS.Parameters.Keys | Should -Contain 'ApplyResultsJsonPath' } It 'Has parameter ReadinessCsvPath' { $script:S6CmdS.Parameters.Keys | Should -Contain 'ReadinessCsvPath' } It 'Has parameter SummaryFileName' { $script:S6CmdS.Parameters.Keys | Should -Contain 'SummaryFileName' } diff --git a/AzLocal.UpdateManagement/Tests/test-run-timings.csv b/AzLocal.UpdateManagement/Tests/test-run-timings.csv index d6569778..5bdb6df6 100644 --- a/AzLocal.UpdateManagement/Tests/test-run-timings.csv +++ b/AzLocal.UpdateManagement/Tests/test-run-timings.csv @@ -10,3 +10,5 @@ TimestampUtc,ModuleVersion,Total,Passed,Failed,Skipped,WallClockSeconds,PesterDu "2026-06-12T14:30:16Z","0.8.75","1182","1143","0","1","82.37","79.88","4.06","Invoke-Tests.ps1","5.1.26100.8655" "2026-06-12T15:26:59Z","0.8.76","1184","1141","4","1","112.14","109.68","10.52","Invoke-Tests.ps1","5.1.26100.8655" "2026-06-12T15:30:21Z","0.8.76","1184","1145","0","1","100.71","98.24","3.89","Invoke-Tests.ps1","5.1.26100.8655" +"2026-06-14T11:50:46Z","0.8.78","1189","1149","1","1","68.28","66.17","5.34","Invoke-Tests.ps1","5.1.26100.8655" +"2026-06-14T11:51:58Z","0.8.78","1189","1150","0","1","49.95","48.7","3.74","Invoke-Tests.ps1","5.1.26100.8655" diff --git a/AzLocal.UpdateManagement/docs/release-history.md b/AzLocal.UpdateManagement/docs/release-history.md index f7ee7b58..f60028ae 100644 --- a/AzLocal.UpdateManagement/docs/release-history.md +++ b/AzLocal.UpdateManagement/docs/release-history.md @@ -4,7 +4,22 @@ > > **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.77 (the current release)**, see the main [README.md](../README.md#whats-new-in-v0877) `What's New in v0.8.77` section. +> **For v0.8.78 (the current release)**, see the main [README.md](../README.md#whats-new-in-v0878) `What's New in v0.8.78` section. + +--- + +### What's New in v0.8.77 + +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`. + +1. **`Start-AzLocalClusterUpdate` (Step.07 main entry)** - production emitted `Error processing cluster '': The property 'UpdateStartWindow' cannot be found on this object.` for clusters whose tag bag did not include `UpdateStartWindow` / `UpdateExclusionsWindow`. The guard now branches on `$clusterTags -is [System.Collections.IDictionary]` and uses `.Contains()` for hashtables / `PSObject.Properties[...]` for `PSCustomObject` tag bags. The semantic intent ("absent = any time eligible / no window restriction") is preserved. +2. **`Test-AzLocalClusterHealth` (Step.05/06 readiness gate)** - production emitted `Checking: ... Error: The property 'healthCheckResult' cannot be found on this object.` for clusters whose ARM update summary genuinely had no `healthCheckResult` field. The `catch` block then flagged the cluster `HealthState=Error / Passed=$false`, poisoning Step.5 / Step.7 / Step.9 readiness output for affected clusters. Same guard idiom applied in `Get-HealthCheckFailureSummary` (Private) and `Get-AzLocalFleetStatusData` (Public, `-IncludeHealthDetails`). Such clusters now correctly classify as `HealthState="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](../CHANGELOG.md#0877---2026-06-14) for the full v0.8.77 entry. ---