diff --git a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/README.md b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/README.md index ba31940d..2ed1e66a 100644 --- a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/README.md +++ b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/README.md @@ -94,6 +94,11 @@ Each row's name links to the matching `Step N -` section in [`docs/appendix-pipe | 10 | [Step.10 - Fleet Health Status](docs/appendix-pipelines.md#step-10---fleet-health-status) | [`fleet-health-status.yml`](github-actions/fleet-health-status.yml) | [`fleet-health-status.yml`](azure-devops/fleet-health-status.yml) | - **GitHub Actions**: the Actions sidebar sorts workflows alphabetically by the `name:` field inside the YAML. Because every `name:` starts with `Step.N - `, the sidebar lists the eleven workflows in execution order (Step.0 first, Step.10 last) instead of the cosmetically confusing alphabetical scatter (`Apply Updates`, `Apply-Updates Schedule Coverage Audit`, `Assess Update Readiness`, ...). + + ![GitHub Actions sidebar of the Azure/AzLocal.UpdateManagement consumer repo showing all eleven workflows listed in execution order (Step.00 Authentication Validation, Step.01 Inventory Azure Local Clusters, Step.02 Manage UpdateRing Tags, Step.03 Apply-Updates Schedule Coverage Audit, Step.04 Fleet Connectivity Status, Step.05 Assess Update Readiness, Step.06 Sideload Updates Opt-in, Step.07 Apply Updates, Step.08 Monitor In-Flight Updates, Step.09 Fleet Update Status, Step.10 Fleet Health Status) with the All workflows pane showing a recent run history of green-checked successful runs across Step.05 / Step.08 / Step.09 / Step.10 from main](../docs/images/github-actions-10-pipelines-view.png) + + *Step.NN display-name prefix means the GitHub Actions sidebar (and the Azure DevOps Pipelines list) sort the eleven workflows in execution order rather than the cosmetically confusing alphabetical scatter.* + - **Azure DevOps**: the Pipelines list sorts by the pipeline **definition name** chosen at *import time* (not by the YAML filename and not by any top-level `name:` field - the `name:` field in an ADO YAML controls the per-run *build number*, not the pipeline display name). When you import each YAML, the import wizard prefills the suggested pipeline name from the YAML's leading title comment; the YAMLs in this repo open with `# Step.N - `, so the suggested name is already correct. **Accept the suggested name** (or paste `Step.N - ` yourself), and the Pipelines list will sort in execution order. You can rename a pipeline later via *Pipeline -> Edit -> Settings -> Name*. If you prefer a different naming scheme (e.g. `00 - Auth`, `01 - Inventory`, ...), just change the `name:` field in each GH Actions YAML and / or pick a different prefix at ADO import time. Nothing else in the module depends on these display names. @@ -1140,7 +1145,7 @@ To set an estate-wide pin in ADO, either change the `moduleVersion` parameter de | Situation | What you see | What it means | |---|---|---| -| `installed > generated` | "Pipeline YAML was generated against AzLocal.UpdateManagement v but the agent installed v." | Your committed YAML is older than the module on the agent. Pipeline steps may have been improved since - re-run `Copy-AzLocalPipelineExample -Update` to refresh. | +| `installed > generated` | "Pipeline YAML was generated against AzLocal.UpdateManagement v but the agent installed v." | Your committed YAML is older than the module on the agent. Pipeline steps may have been improved since - re-run `Update-AzLocalPipelineExample` (with the `-Platform GitHub` / `-Platform AzureDevOps` flag the v0.8.75 annotation now prints for you) to refresh while preserving your `AZLOCAL-CUSTOMIZE` markers. | | `latest > installed` | "AzLocal.UpdateManagement v is available on PSGallery; this run installed v." | A newer module is on PSGallery than the one the pipeline pinned to. Review the [module CHANGELOG](../CHANGELOG.md) before bumping `REQUIRED_MODULE_VERSION` (or clear the pin to install the latest automatically). | Both annotations are warnings, not failures - your pipeline still passes. @@ -1451,10 +1456,18 @@ The "steady-state" phase ships **three complementary pipelines**, all read-only, The four run in distinct (offset) cron slots so they don't contend for the same agent. `monitor-updates.yml` ships **without** an active schedule - turn the `'*/30 * * * *'` cron on only during an active wave. +![Step.08 - Monitor In-Flight Updates summary tab: red Fleet Status CRITICAL header (1 run > 6d, 1 step > 4h, 4 unresolved failures, 20 clusters scoped), the new Tip line explaining Ctrl/Cmd/middle-click for new-tab opens (GitHub markdown strips target="_blank"), the In-flight runs table with the new deep-tree Progress column showing `132/167 steps (79%)` for the Arizona Solution12.2604.1003.1005 CAU Attempt step, and the Failed runs table with plain-anchor Cluster + Update hyperlinks for Toronto / Virginia / NewYorkCity](../docs/images/monitor-inflight-updates.png) + +*Step.08 in-flight monitor on a real 20-cluster fleet - the `Progress` column reports leaf-step completion (`M/N steps (P%)`) since v0.8.74 instead of the coarse top-level wrapper count that previously always read `1/2 steps`, and the `Tip` line above the runs table makes the Ctrl/Cmd/middle-click new-tab behaviour explicit (GitHub markdown strips `target="_blank"` from anchors).* + **Fleet Connectivity Status** *(introduced in v0.7.79, enhanced in v0.7.85)* runs daily at 05:30 UTC and answers the upstream question every other steady-state pipeline depends on: *"can the pipeline identity actually see every cluster, every physical node, and every Resource Bridge it is supposed to manage?"* The Step.4 reconciliation table compares each cluster's `reportedProperties.nodes` count against the Arc-tagged physical machines visible in Resource Graph and flags both directions of drift (positive = Arc has more machines than the cluster reports; negative = cluster reports more nodes than Arc can see). The v0.7.85 *"How to interpret + act on a non-zero reconciliation"* subsection in the pipeline summary gives operators per-direction remediation lists and an inline Resource Graph query template for triage. RBAC: `Reader` plus `Microsoft.ResourceGraph/resources/read`, `Microsoft.AzureStackHCI/edgeDevices/read`, `Microsoft.HybridCompute/machines/read`, and `Microsoft.ResourceConnector/appliances/read` - all already in the **`Azure Stack HCI Update Operator (custom)`** custom role definition shipped in [section 3.1](#31-custom-role-azure-stack-hci-update-operator-custom). **Fleet Update Status** is scheduled to run daily at 06:00 UTC once you push the YAML. It does no writes - it builds a fleet-wide JUnit + CSV + JSON snapshot for dashboards and alerting. +![Step.09 - Fleet Update Status summary tab: Fleet Version Distribution table breaking 20 clusters into 5 YYMM rows (2605 / 2604 / 2603 / 2601 supported, 2511 unsupported) with per-row cluster counts, percentages, support badges and the first 15 cluster names per version row, plus a Critical Health Status table (13 Passed / 7 Failed) and a Primary Status table (Total Clusters 20, Up to Date 7, Ready for Update 5, Update In Progress 1)](../docs/images/fleet-update-status.png) + +*Step.09 Fleet Update Status summary tab on a real 20-cluster fleet - leads with the version-distribution table (anchored on the Microsoft manifest YYMM) and the Primary Status breakdown that uses the same priority cascade (Up to Date / Ready for Update / In Progress / SBE Prerequisite / Health Failure / Update Failed / Action Required / Needs Investigation) as Step.05 and Step.07 since v0.8.74.* + | Artefact | Description | |---|---| | `readiness-status.xml` | JUnit XML, one cluster per test (`Passed` = healthy + up to date, `Failed` = needs attention, `Failed/HasPrerequisite` = vendor SBE update required first). | @@ -1478,6 +1491,14 @@ It calls the new [`Get-AzLocalFleetHealthFailures`](../README.md#get-azlocalflee **RBAC for Fleet Health Status** (read-only): the service connection needs `Reader` on each cluster (or the parent RG / subscription) plus `Microsoft.ResourceGraph/resources/read`. No write actions are taken. +![Step.10 - Fleet Health Status summary tab part 1: Health Check Failures By Reason table sorted most-widespread-first, with Severity / Failure Reason / Cluster Count / Failure Count / Affected Clusters (Seattle, Toronto, Mobile, Nashville, Tacoma, Virginia linked) / Latest columns - leading critical reasons are Storage Services Physical Disks Health Check (2 clusters / 12 failures), Storage Virtual Disk Health Check (2/8), Storage Job Health Check (2/7), Test Network intent on existing cluster nodes (2/5) and several Environment Validator exceptions and HCIOrchestratorAccount / ECEAgentService Account / Cluster-Aware Updating Rule 6 health checks](../docs/images/fleet-health-status-part1.png) + +*Step.10 Fleet Health Status part 1: the pivot-by-failure-reason table that leads the summary. Sorted by `ClusterCount desc` so the most widespread fleet-wide issues bubble to the top - this is the ready-made "what should we fix first?" prioritisation view.* + +![Step.10 - Fleet Health Status summary tab part 2: Detailed Results (per-cluster, per-failure) section with collapsible cluster rows sorted worst-first - Toronto Critical x28, Seattle Critical x27 / Warning x7, Tacoma Critical x9, Nashville Critical x3 / Warning x1 (expanded) showing three [Critical] Environment Validator Exception - Test-AzStackHciHardware rows with Failure Remediation "Raise case with Microsoft support" plus one [Warning] Microsoft.Health.FaultType.Cluster.KeyVaultDoesNotExist row linking out to the Microsoft Learn key-vault remediation guide, then collapsed rows for Mobile, Virginia, NewYorkCity, Bellevue, Portland, ending with Reports Available pointing to fleet-health-detail.csv](../docs/images/fleet-health-status-part-2.png) + +*Step.10 Fleet Health Status part 2: the per-cluster Detailed Results drill-down. Clusters are listed worst-affected first, each expander shows the full per-(cluster, failing check) rows with Failure Reason, Failure Remediation, Target Resource Name / Type, and Last Occurrence - mirroring the standalone "24-Hour System Health Checks - Detailed Results" view.* + Configure your CI/CD platform's alerting on the JUnit failures - GitHub Actions surfaces them in the run summary and Azure DevOps shows them in the Tests tab with trend analytics. **Plus weekly: `apply-updates-schedule-audit.yml`** (read-only, runs Mondays at 05:00 UTC by default) catches drift between the cron schedule(s) committed to `apply-updates.yml` and the `UpdateRing` / `UpdateStartWindow` tags that operators apply to new clusters. It emits a JUnit + CSV + Markdown "Recommend" snippet that pastes straight back into Step.7 to close any coverage gap. **For the full audit runbook (tag a cluster -> see drift -> paste recommended cron -> re-run and watch it turn green), see [section 8.3](#83-end-to-end-runbook-apply-updates-schedule-coverage-audit).** @@ -1724,6 +1745,22 @@ And finally the **ready-to-paste cron block** (Recommend view). With the default # - cron: '0 23 * * 1-5' # Mon-Fri_22:00-04:00 (retry) (rings: Wave2, 47 cluster(s)) ```` +##### Visual: example Step.3 step summary + +The three captures below are from the same Step.3 run on a fleet with a drift case (two clusters tagged with a window the existing crons do not cover) and a NoWindowTag case (one cluster missing the `UpdateStartWindow` tag entirely). + +**Part 1 - Action required (1 of 2): cron coverage remediation** - the recommended `schedule:` / `schedules:` block to paste into `apply-updates.yml`: + +![Step.3 step summary - Action required (1 of 2): cron coverage remediation](../docs/images/apply-updates-schedule-audit-part1.png) + +**Part 2 - Action required (2 of 2): NoWindowTag remediation** - per-cluster advisor with a peer-derived `UpdateStartWindow` value to apply via `Set-AzLocalClusterUpdateRingTag`: + +![Step.3 step summary - Action required (2 of 2): NoWindowTag remediation](../docs/images/apply-updates-schedule-audit-part2.png) + +**Part 3 - Cycle calendar (enriched, 7 columns)** - per-day UTC projection over the schedule's cycle horizon, with the Step.6 apply-updates cron firing times and `Tag Start Window Match (>=95%)` per ring/date so you can verify at-a-glance that each ring's tagged clusters have an `UpdateStartWindow` that actually covers at least one cron firing on its eligible days: + +![Step.3 step summary - Cycle calendar (enriched, 7 columns)](../docs/images/apply-updates-schedule-audit-part3.png) + #### Step 5 - Apply the recommendation Open `apply-updates.yml`, uncomment / paste the recommended `schedule:` (GH) or `schedules:` (ADO) block, and commit. The audit pipeline emits both blocks even when `-Platform Both` is the default - copy the section that matches your CI/CD platform. 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 5882179c..3a0342fa 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.74' + GENERATED_AGAINST_MODULE_VERSION: '0.8.75' 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 9f683ca8..ad408b72 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.74' + value: '0.8.75' # 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/assess-update-readiness.yml b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/assess-update-readiness.yml index 3eee24f7..1d552c56 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.74' + GENERATED_AGAINST_MODULE_VERSION: '0.8.75' # 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 609dabf5..8a7c0636 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.74' + value: '0.8.75' # 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 d0675a3a..88adfa22 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.74' + GENERATED_AGAINST_MODULE_VERSION: '0.8.75' # 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 2998a3bb..0df3fcba 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.74' + GENERATED_AGAINST_MODULE_VERSION: '0.8.75' # 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 fa56748b..4bb79a08 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.74' + GENERATED_AGAINST_MODULE_VERSION: '0.8.75' # 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 86aa14c2..edb9bd09 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.74' + value: '0.8.75' # 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 39a08090..1be87f04 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.74' + value: '0.8.75' # 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 3209a272..0d1694fd 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.74' + GENERATED_AGAINST_MODULE_VERSION: '0.8.75' 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 b879c838..4d680f6a 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.74' + GENERATED_AGAINST_MODULE_VERSION: '0.8.75' # 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/github-actions/apply-updates-schedule-audit.yml b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/apply-updates-schedule-audit.yml index cc0371b2..bc88783e 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.74' + GENERATED_AGAINST_MODULE_VERSION: '0.8.75' 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 68164c4c..91dbe5f3 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.74' + GENERATED_AGAINST_MODULE_VERSION: '0.8.75' # 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/assess-update-readiness.yml b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/assess-update-readiness.yml index 36c76f97..4437656b 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.74' + GENERATED_AGAINST_MODULE_VERSION: '0.8.75' # 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 4fa3ec92..5bbeac9d 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.74' + GENERATED_AGAINST_MODULE_VERSION: '0.8.75' # 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 3e579b02..142dfcce 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.74' + GENERATED_AGAINST_MODULE_VERSION: '0.8.75' # 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 d290bb87..b5be44a6 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.74' + GENERATED_AGAINST_MODULE_VERSION: '0.8.75' # 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 9a77c6e2..9d744d4e 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.74' + GENERATED_AGAINST_MODULE_VERSION: '0.8.75' # 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 5c7d201d..484a1317 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.74' + GENERATED_AGAINST_MODULE_VERSION: '0.8.75' # 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 194816c7..1d2323b7 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.74' + GENERATED_AGAINST_MODULE_VERSION: '0.8.75' # 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 82840974..342ff668 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.74' + GENERATED_AGAINST_MODULE_VERSION: '0.8.75' # 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 9c981116..dad18bd3 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.74' + GENERATED_AGAINST_MODULE_VERSION: '0.8.75' # 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 da7637d7..2de0c3a0 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.74' + ModuleVersion = '0.8.75' # Supported PSEditions CompatiblePSEditions = @('Desktop', 'Core') @@ -309,16 +309,14 @@ # ReleaseNotes of this module ReleaseNotes = @' -## 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). No public API or export-count change (still 60). - -- **FIX (reporting)**: a cluster that has already applied every required update was being mis-classified. Step.5 (`Export-AzLocalClusterUpdateReadinessReport`) showed `0` in its "Up to date" summary count and listed fully-patched clusters in the "Not-Ready clusters (review first)" table; Step.7 (`Export-AzLocalClusterReadinessGateReport`) rendered them with a no-entry icon in a binary `Ready?` column, implying failure. Root cause: the "Up to Date" test required `AllAvailableUpdates` to be empty, but a cluster that has installed all updates still lists those (now-Installed) package names there, so the strict test never matched. -- **FIX (Progress column)**: the `Progress` column rendered by the Step.7 in-flight monitor and the standalone HTML "Recent Update Run History" table was always reporting the coarse top-level wrapper count (typically `1/2 steps`) for the whole multi-hour run because Azure Local only exposes two top-level wrapper steps (`Prepare update` + `Start update`) and `Start update` stays `InProgress` end-to-end. Both paths now traverse 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`), so the Progress column climbs as the install proceeds and matches how `CurrentStep` already walks the tree via `Get-DeepestActiveStep`. -- **FIX (portal hyperlinks)**: the Cluster / Update portal hyperlinks in the Step.7 (monitor), Step.8 (fleet update status) and Step.10 (fleet health status) step-summary tables always opened in the current tab even though the markdown set `target="_blank"`. GitHub's GFM sanitiser (and the ADO equivalent) strips `target` entirely and forces `rel="nofollow"` - confirmed in the rendered HTML of a real GHA job summary. The dead `target="_blank"` / `rel="noopener"` 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." The standalone HTML report (`New-AzLocalFleetStatusHtmlReport`) is unaffected - it ships as a raw `.html` artifact, not a step-summary. -- **NEW (internal)**: `Get-AzLocalClusterReadinessStatus` private helper is now 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`) all classify clusters identically via this helper - previously each re-implemented the logic inline and the definitions had drifted. -- **CHANGE (Step.7)**: the per-cluster readiness table's binary `Ready?` column is replaced with a readable `Status` column (`Ready`, `Up to Date`, `In Progress`, `SBE Prerequisite`, `Health Failure`, `Update Failed`, `Action Required`, `Needs Investigation`). The header now shows a distinct `Up to Date` count and a new `UP_TO_DATE_COUNT` / `UpToDateCount` step output is emitted (additive; existing `READY_COUNT` gate semantics are unchanged). PassThru gains `UpToDateCount`. -- **CHANGE (Step.5)**: the "All clusters detail" table's `Ready` boolean column is replaced with the same readable `Status` label, and the "Not-Ready clusters (review first)" table now excludes Up-to-Date and Ready clusters. -- **CHANGE (Step.7)**: the "No Clusters Ready" summary (`Add-AzLocalNoReadyClustersStepSummary`) gains `-UpToDateCount` / `-NotReadyCount` parameters and renders an Up-to-Date vs Not-Ready breakdown so idle runs show that already-patched clusters are a healthy steady state (a notice, not a warning, when all are up to date). Both apply-updates templates wire the counts through. -- **All bundled pipeline templates** bump `GENERATED_AGAINST_MODULE_VERSION` from `'0.8.73'` to `'0.8.74'`. +## 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). 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. Clean-fleet runs (no findings) were unaffected because the snippet is never written when its items list is empty. New additive `-OmitCycleCalendar` switch on `Test-AzLocalApplyUpdatesScheduleCoverage`; `Export-AzLocalApplyUpdatesScheduleAudit` opts in so only the enriched calendar surfaces regardless of findings. Direct callers of the Recommend view see no behaviour change. Drift-notice banner now recommends `Update-AzLocalPipelineExample` (marker-aware merge) with the right `-Platform` and a `-Destination` hint inferred from the detected pipeline host. Section 8.3 of `Automation-Pipeline-Examples/README.md` gains three Step.3 step-summary screenshots. No public API or export-count change (still 60). + +- **FIX (Step.3 step summary)**: the cycle-calendar table was rendered twice on any run with Recommend-view findings - once by `Test-AzLocalApplyUpdatesScheduleCoverage -View Recommend` (plain 5-col, inlined into the wrapper as the action-required block when `$hasIssues` is true), and once by the wrapper itself (enriched 7-col with per-day cron firings + `Tag Start Window Match (>=95%)`). Clean fleets were unaffected because the Recommend snippet is only written when its items list is non-empty. Additive `[switch]$OmitCycleCalendar` parameter on `Test-AzLocalApplyUpdatesScheduleCoverage` (default off) suppresses the inner emission; `Export-AzLocalApplyUpdatesScheduleAudit` opts in so a single enriched calendar renders in every scenario. New Pester regression asserts the switch is bound + exactly one `## Cycle calendar - next` heading rendered. +- **CHANGED (drift-notice banner)**: `Add-AzLocalPipelineVersionBanner` now recommends `Update-AzLocalPipelineExample` (marker-aware merge, preserves `AZLOCAL-CUSTOMIZE` blocks) instead of `Copy-AzLocalPipelineExample -Update` (clean overwrite). Inlines the right `-Platform` and `-Destination` hint per detected host: `'\.github\workflows' -Platform GitHub` on GH, `'\pipelines' -Platform AzureDevOps` on ADO. Symmetric YAML-newer warning gets the same treatment. PassThru verdict is `'YAML older than module - run Update-AzLocalPipelineExample to refresh'`. +- **DOCS**: 3 Step.3 screenshots added to `Automation-Pipeline-Examples/README.md` section 8.3 (cron coverage, NoWindowTag, enriched cycle calendar). `docs/images/README.md` referenced-by table updated. +- **All bundled pipeline templates** bump `GENERATED_AGAINST_MODULE_VERSION` from `'0.8.74'` to `'0.8.75'`. + +## 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.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). diff --git a/AzLocal.UpdateManagement/AzLocal.UpdateManagement.psm1 b/AzLocal.UpdateManagement/AzLocal.UpdateManagement.psm1 index ed5017ce..ea15d2ba 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.74' +$script:ModuleVersion = '0.8.75' $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 f0afa63f..abc3815f 100644 --- a/AzLocal.UpdateManagement/CHANGELOG.md +++ b/AzLocal.UpdateManagement/CHANGELOG.md @@ -5,6 +5,32 @@ 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.75] - 2026-06-12 + +Patch release. Removes the duplicate **Cycle calendar** table from the Step.3 apply-updates schedule audit step summary - before this fix, on any run where the Recommend view had at least one action-required finding (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 output **above** the wrapper's own enriched 7-column calendar, so operators saw two calendars back-to-back. Clean-fleet runs (no findings) were unaffected because the Recommend snippet is never written when its items list is empty. Also lands three Step.3 step-summary screenshots in `Automation-Pipeline-Examples/README.md` section 8.3. No public API or export-count change (still 60). + +### Fixed + +- **Step.3 step summary rendered the Cycle calendar twice when there were findings.** `Test-AzLocalApplyUpdatesScheduleCoverage -View Recommend` writes a self-contained snippet to its `schedule-coverage-recommend.md` output - the snippet carries its action-required H2s, the proposed cron YAML block, AND a plain (5-column) cycle-calendar block (`Date | Day | CycleWeek | Eligible rings | AllowedUpdateVersions`). That snippet is only written when the Recommend view emits at least one row (so on a clean fleet, `$output.Count` is 0 and nothing is exported). `Export-AzLocalApplyUpdatesScheduleAudit` (the Step.3 wrapper) inlines that file as the action-required block when `$hasIssues` is true, and ALSO renders its OWN enriched (7-column) calendar later in the summary - with two extra columns (`Ring CRON Start Time (apply-updates pipeline)` and `Tag Start Window Match (>=95%)`) computed from the Step.6 cron triggers + cluster CSV. The wrapper used to be the only emitter; the inner emission was added in v0.8.5 (as a regression guard against a v0.8.4 silent-drop bug on clean fleets) and the two paths coexisted without anyone noticing the duplicate until v0.8.74's production Step.3 run. +- The fix is a new additive `[switch]$OmitCycleCalendar` parameter on `Test-AzLocalApplyUpdatesScheduleCoverage` (default off, no behaviour change for direct callers) that suppresses the inner calendar; `Export-AzLocalApplyUpdatesScheduleAudit` now opts in (`-OmitCycleCalendar=$true`) so only the enriched calendar surfaces in the Step.3 summary regardless of whether the run has findings. + +### Changed (drift-notice banner) + +- **`Add-AzLocalPipelineVersionBanner` now recommends `Update-AzLocalPipelineExample` (not `Copy-AzLocalPipelineExample -Update`) in the YAML-older-than-module annotation, and inlines the correct `-Platform` value and a sensible `-Destination` hint based on the detected pipeline host.** Before v0.8.75 the annotation said *"re-run 'Copy-AzLocalPipelineExample -Update' (you will be prompted per file; add `-Confirm:$false` to bypass)"* - which (a) used the clean-overwrite tool when the marker-aware merge tool exists for exactly this scenario, and (b) left the operator to figure out the right `-Platform` and `-Destination` themselves. The new annotation prints the complete copy-pasteable command: `Update-AzLocalPipelineExample -Destination '\.github\workflows' -Platform GitHub` on GitHub Actions, `Update-AzLocalPipelineExample -Destination '\pipelines' -Platform AzureDevOps` on Azure DevOps. Update preserves customer customisations bracketed by `AZLOCAL-CUSTOMIZE` markers (CRON schedules, ITSM secrets) so it is the correct tool to recommend on a repo that has already been customised. The symmetric YAML-newer-than-module warning gets the same treatment. Verdict text on `-PassThru` returns `'YAML older than module - run Update-AzLocalPipelineExample to refresh'`. + +### Added + +- **`Test-AzLocalApplyUpdatesScheduleCoverage -OmitCycleCalendar` switch** - opts out of the Recommend view's informational cycle calendar section. Used by `Export-AzLocalApplyUpdatesScheduleAudit` to avoid the duplicate-rendering bug above; direct callers see the calendar unchanged. +- New Pester regression test (`v0.8.75: passes -OmitCycleCalendar to the inner Recommend invocation so the step summary renders only one Cycle calendar`) spies on the Recommend mock to assert the switch is bound, and asserts the rendered step summary contains exactly one `## Cycle calendar - next` heading. + +### Docs + +- `Automation-Pipeline-Examples/README.md` section 8.3 gains three Step.3 step-summary screenshots: `apply-updates-schedule-audit-part1.png` (cron coverage remediation), `apply-updates-schedule-audit-part2.png` (NoWindowTag remediation), `apply-updates-schedule-audit-part3.png` (enriched cycle calendar). `docs/images/README.md` referenced-by table updated with the three new entries. + +### Changed + +- All bundled pipeline templates bump `GENERATED_AGAINST_MODULE_VERSION` from `'0.8.74'` to `'0.8.75'`. + ## [0.8.74] - 2026-06-12 Consistency release for the cluster readiness reports. A cluster that has already applied every required update is now classified and labelled identically across the Step.5 readiness report, the Step.7 readiness gate, and the Step.9 fleet update-status report. Also corrects the `Progress` column in the Step.7 in-flight monitor and the standalone HTML report's "Recent Update Run History" table, which had been reporting a near-constant `1/2 steps` for the whole multi-hour run, and drops the dead `target="_blank"` / `rel="noopener"` attributes from every Step.7 / Step.8 / Step.10 portal-link cell (GitHub Actions and Azure DevOps step-summary sanitisers strip them anyway) in favour of an explicit Ctrl/Cmd/middle-click tip rendered above the affected tables. No public API or export-count change (still 60). diff --git a/AzLocal.UpdateManagement/Public/Add-AzLocalPipelineVersionBanner.ps1 b/AzLocal.UpdateManagement/Public/Add-AzLocalPipelineVersionBanner.ps1 index 20170e8a..9ae10cd0 100644 --- a/AzLocal.UpdateManagement/Public/Add-AzLocalPipelineVersionBanner.ps1 +++ b/AzLocal.UpdateManagement/Public/Add-AzLocalPipelineVersionBanner.ps1 @@ -85,7 +85,7 @@ function Add-AzLocalPipelineVersionBanner { Verdict [string] - one of: 'in sync' 'YAML newer than module - check REQUIRED_MODULE_VERSION' - 'YAML older than module - run Copy-AzLocalPipelineExample -Update' + 'YAML older than module - run Update-AzLocalPipelineExample to refresh' 'newer module available on PSGallery' .EXAMPLE @@ -152,10 +152,20 @@ function Add-AzLocalPipelineVersionBanner { $pinStatus = if ([string]::IsNullOrWhiteSpace($PinnedVersion)) { 'latest (fix-forward)' } else { "pinned to v$PinnedVersion" } $latestStr = if ($SkipPSGalleryQuery) { '(PSGallery lookup skipped)' } elseif ($latest) { "v$latest" } else { '(PSGallery lookup failed)' } $verdict = if ($installed -lt $generated) { 'YAML newer than module - check REQUIRED_MODULE_VERSION' } - elseif ($installed -gt $generated) { 'YAML older than module - run Copy-AzLocalPipelineExample -Update' } + elseif ($installed -gt $generated) { 'YAML older than module - run Update-AzLocalPipelineExample to refresh' } elseif ($latest -and ($latest -gt $installed)) { 'newer module available on PSGallery' } else { 'in sync' } + # v0.8.75: tailor the refresh-command hint to the detected pipeline host so the + # operator can copy-paste the exact -Platform and a sensible -Destination from the + # annotation. The customer runs the refresh from their LAPTOP (not the runner), + # so the destination uses a `` placeholder rather than the runner's + # workspace path. Falls back to GitHub conventions for local invocations. + $hostKind = Get-AzLocalPipelineHost + $refreshPlatform = if ($hostKind -eq 'AzureDevOps') { 'AzureDevOps' } else { 'GitHub' } + $refreshDestHint = if ($hostKind -eq 'AzureDevOps') { '\pipelines' } else { '\.github\workflows' } + $refreshCommand = "Update-AzLocalPipelineExample -Destination '$refreshDestHint' -Platform $refreshPlatform" + Write-Host '' Write-Host 'Module version summary' Write-Host " Installed on runner : $installed" @@ -168,12 +178,12 @@ function Add-AzLocalPipelineVersionBanner { if ($installed -lt $generated) { Write-AzLocalPipelineWarning ` -Title "$ModuleName is older than workflow YAML expects" ` - -Message "$ModuleName v$installed is OLDER than the version this workflow YAML was generated against (v$generated). Cmdlets, parameters, or output schemas referenced by this YAML may not exist in v$installed. Set REQUIRED_MODULE_VERSION to v$generated, or refresh the YAML to match the installed module via 'Copy-AzLocalPipelineExample -Update'." + -Message "$ModuleName v$installed is OLDER than the version this workflow YAML was generated against (v$generated). Cmdlets, parameters, or output schemas referenced by this YAML may not exist in v$installed. Either pin REQUIRED_MODULE_VERSION to v$generated so the runner installs the matching version, or refresh the YAML to match the installed module by running (from your laptop): $refreshCommand. Update preserves your customisations bracketed by AZLOCAL-CUSTOMIZE markers (CRON schedules, ITSM secrets, etc.)." } if ($installed -gt $generated) { Write-AzLocalPipelineNotice ` -Title 'Workflow YAML may be stale' ` - -Message "Workflow YAML was generated against $ModuleName v$generated but the runner installed v$installed. Pipeline steps may have been improved in later releases - to refresh, re-run 'Copy-AzLocalPipelineExample -Update' (you will be prompted per file; add -Confirm:`$false to bypass). Pipeline YAMLs are under git so 'git diff' shows exactly what changed before commit." + -Message "Workflow YAML was generated against $ModuleName v$generated but the runner installed v$installed. Pipeline steps may have been improved in later releases. From your laptop, refresh with: $refreshCommand. Update preserves your customisations bracketed by AZLOCAL-CUSTOMIZE markers (CRON schedules, ITSM secrets, etc.). Pipeline YAMLs are under git so 'git diff' shows exactly what changed before commit." } if ($latest -and ($latest -gt $installed)) { Write-AzLocalPipelineNotice ` diff --git a/AzLocal.UpdateManagement/Public/Export-AzLocalApplyUpdatesScheduleAudit.ps1 b/AzLocal.UpdateManagement/Public/Export-AzLocalApplyUpdatesScheduleAudit.ps1 index d75439f4..b60aa2a3 100644 --- a/AzLocal.UpdateManagement/Public/Export-AzLocalApplyUpdatesScheduleAudit.ps1 +++ b/AzLocal.UpdateManagement/Public/Export-AzLocalApplyUpdatesScheduleAudit.ps1 @@ -313,6 +313,11 @@ function Export-AzLocalApplyUpdatesScheduleAudit { } if ($haveSchedule) { $recoArgs.SchedulePath = $SchedulePath } if ($haveCsv) { $recoArgs.ClusterCsvPath = $ClusterCsvPath } + # v0.8.75: suppress the inner plain (5-column) cycle calendar from + # the Recommend output; this wrapper renders its own enriched + # (7-column) calendar below, so without this switch the Step.3 + # step summary would render two cycle-calendar tables back-to-back. + $recoArgs.OmitCycleCalendar = $true Test-AzLocalApplyUpdatesScheduleCoverage @recoArgs | Out-Null # ---- Bucketise -------------------------------------------------------- diff --git a/AzLocal.UpdateManagement/Public/Test-AzLocalApplyUpdatesScheduleCoverage.ps1 b/AzLocal.UpdateManagement/Public/Test-AzLocalApplyUpdatesScheduleCoverage.ps1 index fd9219b9..ae9e2ff8 100644 --- a/AzLocal.UpdateManagement/Public/Test-AzLocalApplyUpdatesScheduleCoverage.ps1 +++ b/AzLocal.UpdateManagement/Public/Test-AzLocalApplyUpdatesScheduleCoverage.ps1 @@ -144,6 +144,13 @@ function Test-AzLocalApplyUpdatesScheduleCoverage { export the table; Recommend exports the YAML snippet. .PARAMETER PassThru Emit objects to the pipeline even when -ExportPath was supplied. + .PARAMETER OmitCycleCalendar + Suppress the informational Cycle calendar section in -View Recommend + output. Used by Export-AzLocalApplyUpdatesScheduleAudit (v0.8.75+) + which inlines this Recommend output AND separately renders its own + enriched (7-column) cycle calendar - the inner plain calendar would + otherwise appear twice in the Step.3 step summary. Default off, + so direct callers see the calendar unchanged. .OUTPUTS PSCustomObject[] - shape depends on -View (see Status values above). .EXAMPLE @@ -201,7 +208,10 @@ function Test-AzLocalApplyUpdatesScheduleCoverage { [string]$ExportPath, [Parameter(Mandatory = $false)] - [switch]$PassThru + [switch]$PassThru, + + [Parameter(Mandatory = $false)] + [switch]$OmitCycleCalendar ) # v0.7.75: auto-detect CI host when the caller accepted the default @@ -940,7 +950,13 @@ resources # it to the cmdlet so the calendar surfaces 'Clusters in # ring(s)' per day and 'Cluster count' per ring. The cmdlet # itself does no CSV / Azure I/O. - if ($scheduleCfg) { + # + # v0.8.75: gate the whole block on -OmitCycleCalendar so the + # Export-AzLocalApplyUpdatesScheduleAudit wrapper (which + # inlines this Recommend output AND separately renders an + # enriched 7-column calendar) does not show two calendars + # in the Step.3 step summary. + if ($scheduleCfg -and -not $OmitCycleCalendar) { $ringCountMap = @{} foreach ($c in @($clusters)) { if ($c.UpdateRing -and -not [string]::IsNullOrWhiteSpace($c.UpdateRing)) { diff --git a/AzLocal.UpdateManagement/README.md b/AzLocal.UpdateManagement/README.md index cea10b51..5aa59789 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.74 - [Published in PowerShell Gallery](https://www.powershellgallery.com/packages/AzLocal.UpdateManagement/0.8.74) +**Latest Version:** v0.8.75 - [Published in PowerShell Gallery](https://www.powershellgallery.com/packages/AzLocal.UpdateManagement/0.8.75) > 📢 **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.74](#whats-new-in-v0874) +- [What's New in v0.8.75](#whats-new-in-v0875) - [Files](#files) - [Prerequisites](#prerequisites) - [RBAC Requirements](#rbac-requirements) (summary; full reference in [docs/rbac.md](docs/rbac.md)) @@ -86,22 +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.74 +## What's New in v0.8.75 -**Consistent "Up to Date" classification across the readiness reports, a deep-tree fix for the `Progress` column in the Step.7 monitor and the standalone HTML "Recent Update Run History" report, and removal of the dead `target="_blank"` portal-link attributes that GitHub / ADO step-summary sanitisers were stripping anyway (replaced with an explicit Ctrl/Cmd/middle-click tip above each affected table).** A cluster that has already applied every required update is now classified and labelled identically across the Step.5 readiness report, the Step.7 readiness gate, and the Step.9 fleet update-status report. The Step.7 monitor and the standalone HTML report now report `Progress` as deep leaf-step completion (`M/N steps (P%)`) instead of the coarse top-level wrapper count (`1/2 steps`) that they had been emitting for the whole multi-hour run. No public API or export-count change (still 60). +**Patch release. Removes the duplicate Cycle calendar table from the Step.3 apply-updates schedule audit step summary** - before v0.8.75, on any run with at least one Recommend-view action-required finding (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 output (as part of the action-required block) **above** the wrapper's own enriched 7-column calendar, so operators saw two calendars back-to-back. Clean-fleet runs (no findings) were unaffected because the Recommend snippet is never written when its items list is empty. Also lands three Step.3 step-summary screenshots in `Automation-Pipeline-Examples/README.md` section 8.3. No public API or export-count change (still 60). -1. **Fixed**: fully-patched clusters were mis-classified as "not ready". Step.5 (`Export-AzLocalClusterUpdateReadinessReport`) showed `0` in its "Up to date" summary count and listed such clusters in the "Not-Ready clusters (review first)" table; Step.7 (`Export-AzLocalClusterReadinessGateReport`) rendered them with a no-entry icon in the binary `Ready?` column - both implying failure. Root cause: the "Up to Date" test required the `AllAvailableUpdates` collection to be empty, but a cluster that has installed all updates still lists those (now-Installed) package names there, so the test never matched. Step.9 already classified these correctly; Step.5/Step.7 now share that exact logic. -2. **Fixed (Progress column)**: the `Progress` column rendered by the Step.7 in-flight monitor (`Export-AzLocalUpdateRunMonitorReport`) and the standalone HTML report's `Recent Update Run History` table (`New-AzLocalFleetStatusHtmlReport` / `Get-AzLocalFleetStatusData`) always reported the top-level wrapper count (typically `1/2 steps`) because Azure Local only exposes two top-level wrapper steps in `properties.progress.steps` (`Prepare update` + `Start update`) and `Start update` stays `InProgress` for the whole run. Both paths now traverse 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`. Matches how `CurrentStep` already walks the tree via `Get-DeepestActiveStep`. -3. **Fixed (portal hyperlinks)**: the Cluster / Update hyperlinks in Step.7 (`Export-AzLocalUpdateRunMonitorReport`), Step.8 (`Export-AzLocalFleetUpdateStatusReport`) and Step.10 (`Export-AzLocalFleetHealthStatusReport`) step-summary tables always opened in the current tab even though the markdown source set `target="_blank"`. GitHub's GFM sanitiser (and the ADO equivalent) strips `target` entirely and forces `rel="nofollow"` - confirmed in the rendered HTML of a real job summary. The dead attributes are now removed 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 ships as a raw `.html` artifact, not a step-summary, so its `target="_blank"` already works. -4. **Added**: `Get-AzLocalClusterReadinessStatus` private helper 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`) all classify clusters through this one helper. -5. **Added**: `Get-AzLocalUpdateRunStepStats` private helper recursively walks the nested `properties.progress.steps` tree and returns leaf-step totals (`TotalLeaf` / `CompletedLeaf` / `InProgressLeaf` / `FailedLeaf`). `Skipped` leaves count as completed so a fully-finished run reaches 100%; wrapper steps are excluded so children aren't double-counted. -6. **Added**: Step.7 emits a new `UP_TO_DATE_COUNT` / `UpToDateCount` step output (additive; existing `READY_COUNT` gate semantics unchanged) and `-PassThru` returns `UpToDateCount`. -7. **Changed**: Step.7's per-cluster table replaces the binary `Ready?` column with a readable `Status` column (`Ready`, `Up to Date`, `In Progress`, `SBE Prerequisite`, `Health Failure`, `Update Failed`, `Action Required`, `Needs Investigation`) and the header line shows a distinct `Up to Date` count. Step.5's "All clusters detail" table replaces its `Ready` boolean column with the same readable `Status` label, and its "Not-Ready clusters (review first)" table excludes Up-to-Date and Ready clusters. -8. **Changed**: the Step.7 "No Clusters Ready" summary now explains *why* there is nothing to apply. `Add-AzLocalNoReadyClustersStepSummary` takes new `-UpToDateCount` / `-NotReadyCount` parameters and renders an Up-to-Date vs Not-Ready breakdown table - so an idle run makes clear that already-patched clusters are a healthy steady state (and logs a notice, not 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. +1. **Fixed**: the Step.3 step summary rendered the Cycle calendar twice on runs with findings. `Test-AzLocalApplyUpdatesScheduleCoverage -View Recommend` writes a self-contained snippet to its `schedule-coverage-recommend.md` output - the snippet carries its action-required H2s, the proposed cron YAML block, AND a plain (5-column) cycle-calendar block (`Date | Day | CycleWeek | Eligible rings | AllowedUpdateVersions`). That snippet is only written when the Recommend view emits at least one row (clean fleets skip it). `Export-AzLocalApplyUpdatesScheduleAudit` (the Step.3 wrapper) inlined that file as the action-required block when `$hasIssues` was true AND then rendered its OWN enriched (7-column) calendar lower in the summary - with two extra columns (`Ring CRON Start Time (apply-updates pipeline)` and `Tag Start Window Match (>=95%)`) computed from the Step.6 cron triggers + cluster CSV. +2. **Added**: new additive `[switch]$OmitCycleCalendar` parameter on `Test-AzLocalApplyUpdatesScheduleCoverage` (default off, so direct callers see the calendar unchanged). `Export-AzLocalApplyUpdatesScheduleAudit` passes `-OmitCycleCalendar=$true` so only the enriched calendar surfaces in the Step.3 summary regardless of whether the run has findings. +3. **Added**: Pester regression spies on the Recommend mock to assert the switch is bound, and asserts the rendered step summary contains exactly one `## Cycle calendar - next` heading. +4. **Changed (drift-notice banner)**: `Add-AzLocalPipelineVersionBanner` now recommends `Update-AzLocalPipelineExample` (the marker-aware merge tool that preserves `AZLOCAL-CUSTOMIZE` blocks) instead of `Copy-AzLocalPipelineExample -Update` (the clean-overwrite tool) in the YAML-older-than-module annotation, and inlines the right `-Platform` value plus a sensible `-Destination` hint based on the detected pipeline host: `Update-AzLocalPipelineExample -Destination '\.github\workflows' -Platform GitHub` on GitHub Actions, `Update-AzLocalPipelineExample -Destination '\pipelines' -Platform AzureDevOps` on Azure DevOps. Symmetric warning when the YAML is newer than the installed module gets the same treatment. Three new Pester cases pin the GH / ADO / verdict text. +5. **Docs**: section 8.3 of `Automation-Pipeline-Examples/README.md` (Apply-Updates Schedule Coverage Audit runbook) gains three Step.3 step-summary screenshots - cron coverage remediation, NoWindowTag remediation, and the enriched cycle calendar. -`GENERATED_AGAINST_MODULE_VERSION` bumped from `0.8.73` to `0.8.74` across all bundled pipeline templates. +`GENERATED_AGAINST_MODULE_VERSION` bumped from `0.8.74` to `0.8.75` across all bundled pipeline templates. -See [CHANGELOG.md](CHANGELOG.md#0874---2026-06-11) for the full v0.8.74 entry. See [`What's New in v0.8.73`](#whats-new-in-v0873) in the Release History section below for the previous release. +See [CHANGELOG.md](CHANGELOG.md#0875---2026-06-12) for the full v0.8.75 entry. See [`What's New in v0.8.74`](docs/release-history.md#whats-new-in-v0874) in the Release History for the previous release. ## Files @@ -580,7 +577,7 @@ 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.74** stay above under [`What's New in v0.8.74`](#whats-new-in-v0874). +The most recent release notes for **v0.8.75** stay above under [`What's New in v0.8.75`](#whats-new-in-v0875). ### What's New in v0.8.73 diff --git a/AzLocal.UpdateManagement/Tests/AzLocal.UpdateManagement.Tests.ps1 b/AzLocal.UpdateManagement/Tests/AzLocal.UpdateManagement.Tests.ps1 index 3d562e05..22d0df23 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.74' { - $script:ModuleInfo.Version | Should -Be '0.8.74' + It 'Should have version 0.8.75' { + $script:ModuleInfo.Version | Should -Be '0.8.75' } It 'Module version constants are in sync between .psm1 and .psd1' { @@ -12833,6 +12833,46 @@ Describe 'Thin-YAML foundation: Add-AzLocalPipelineVersionBanner' { $info.Verdict | Should -Match 'YAML older than module' } + It "v0.8.75: YAML-older notice includes platform-aware Update-AzLocalPipelineExample command (GitHub host)" { + $env:GITHUB_ACTIONS = 'true' + $env:GITHUB_OUTPUT = $script:_ghOutputFile + $env:GITHUB_STEP_SUMMARY = $script:_ghSummaryFile + $env:TF_BUILD = $null + $captured = & { + InModuleScope AzLocal.UpdateManagement { + Add-AzLocalPipelineVersionBanner -GeneratedAgainstVersion '0.0.1' -SkipPSGalleryQuery + } + } *>&1 + $msg = ($captured -join "`n") + $expectedSubstring = "Update-AzLocalPipelineExample -Destination '\.github\workflows' -Platform GitHub" + $msg | Should -Match '::notice title=Workflow YAML may be stale::' + ($msg.Contains($expectedSubstring)) | Should -BeTrue -Because "the GH notice must include literal substring '$expectedSubstring'" + $msg | Should -Not -Match 'Copy-AzLocalPipelineExample -Update' + } + + It "v0.8.75: YAML-older notice includes platform-aware Update-AzLocalPipelineExample command (AzureDevOps host)" { + $env:GITHUB_ACTIONS = $null + $env:TF_BUILD = 'True' + $env:BUILD_ARTIFACTSTAGINGDIRECTORY = $script:_adoStageDir + $captured = & { + InModuleScope AzLocal.UpdateManagement { + Add-AzLocalPipelineVersionBanner -GeneratedAgainstVersion '0.0.1' -SkipPSGalleryQuery + } + } *>&1 + $msg = ($captured -join "`n") + $expectedSubstring = "Update-AzLocalPipelineExample -Destination '\pipelines' -Platform AzureDevOps" + ($msg.Contains($expectedSubstring)) | Should -BeTrue -Because "the ADO notice must include literal substring '$expectedSubstring'" + $msg | Should -Match 'Workflow YAML may be stale' + $msg | Should -Not -Match 'Copy-AzLocalPipelineExample -Update' + } + + It "v0.8.75: YAML-older verdict (PassThru) reads 'run Update-AzLocalPipelineExample to refresh'" { + $info = InModuleScope AzLocal.UpdateManagement { + Add-AzLocalPipelineVersionBanner -GeneratedAgainstVersion '0.0.1' -SkipPSGalleryQuery -PassThru + } + $info.Verdict | Should -Be 'YAML older than module - run Update-AzLocalPipelineExample to refresh' + } + It 'Find-Module failure is swallowed (returns $null latest, no throw)' { $env:GITHUB_ACTIONS = 'true' $env:GITHUB_OUTPUT = $script:_ghOutputFile @@ -16827,6 +16867,49 @@ on: if (Test-Path -LiteralPath $csvPath) { Remove-Item -LiteralPath $csvPath -Force -ErrorAction SilentlyContinue } } } + + It 'v0.8.75: passes -OmitCycleCalendar to the inner Recommend invocation so the step summary renders only one Cycle calendar' { + # Regression guard: Test-AzLocalApplyUpdatesScheduleCoverage -View + # Recommend emits its own plain (5-column) cycle calendar; the wrapper + # ALSO emits its own enriched (7-column) cycle calendar via + # Get-AzLocalApplyUpdatesScheduleCycleCalendar. Before v0.8.75 the + # Step.3 step summary rendered both back-to-back; the fix is to pass + # -OmitCycleCalendar=$true to the Recommend invocation so only the + # enriched calendar surfaces. Spy on the Recommend mock and assert + # the switch was bound. + $env:GITHUB_ACTIONS = 'true' + $env:GITHUB_OUTPUT = $script:_s3_ghOutputFile + $env:GITHUB_STEP_SUMMARY = $script:_s3_ghSummaryFile + $env:_S3_OUTDIR = $script:_s3_outDir + $env:_S3_PIPELINEDIR = $script:_s3_pipelineDir + $env:_S3_SCHEDULE = $script:_s3_scheduleFile + + $global:_s3_auditRows = @( (New-S3AuditRow -Status 'Covered') ) + $global:_s3_schedule = New-S3ScheduleV2 + $global:_s3_recoOmitCalendar = $null + + $null = InModuleScope AzLocal.UpdateManagement { + Mock Test-AzLocalApplyUpdatesScheduleCoverage { $global:_s3_auditRows } -ParameterFilter { $View -eq 'Audit' } + Mock Test-AzLocalApplyUpdatesScheduleCoverage { + $global:_s3_recoOmitCalendar = [bool]$OmitCycleCalendar + } -ParameterFilter { $View -eq 'Recommend' } + Mock Test-AzLocalApplyUpdatesScheduleCoverage { } -ParameterFilter { $View -eq 'Matrix' } + Mock Get-AzLocalApplyUpdatesScheduleConfig { $global:_s3_schedule } + Mock Get-AzLocalApplyUpdatesScheduleCycleCalendar { '## Cycle calendar - next 4 day(s) (cycle length: 1 week(s))' } + Export-AzLocalApplyUpdatesScheduleAudit ` + -PipelineYamlPath $env:_S3_PIPELINEDIR ` + -SchedulePath $env:_S3_SCHEDULE ` + -OutputDirectory $env:_S3_OUTDIR ` + -PassThru + } + + $global:_s3_recoOmitCalendar | Should -BeTrue -Because 'Export wrapper must pass -OmitCycleCalendar=$true so the Recommend view does not emit a duplicate plain calendar' + + # And the rendered step summary must contain exactly ONE "## Cycle calendar - next" heading. + $summary = Get-Content -LiteralPath $script:_s3_ghSummaryFile -Raw + $heads = [regex]::Matches($summary, '##\s+Cycle calendar - next\s+\d+\s+day') + $heads.Count | Should -Be 1 -Because 'After the fix the Step.3 step summary renders exactly one cycle calendar (the enriched one from the wrapper)' + } } #endregion v0.8.5: Export-AzLocalApplyUpdatesScheduleAudit diff --git a/AzLocal.UpdateManagement/Tests/Pii-Guard.Tests.ps1 b/AzLocal.UpdateManagement/Tests/Pii-Guard.Tests.ps1 index 3a07736c..4542d78b 100644 --- a/AzLocal.UpdateManagement/Tests/Pii-Guard.Tests.ps1 +++ b/AzLocal.UpdateManagement/Tests/Pii-Guard.Tests.ps1 @@ -68,7 +68,11 @@ Describe 'PII Guard: Tests folder' { ) $script:TargetFiles = Get-ChildItem -Path $script:TestsRoot -File -Recurse | - Where-Object { $_.Name -ne $script:GuardFileName -and $_.Extension -in @('.ps1','.psm1','.psd1','.json','.md','.xml','.yml','.yaml','.csv') } + Where-Object { + $_.Name -ne $script:GuardFileName -and + $_.Extension -in @('.ps1','.psm1','.psd1','.json','.md','.xml','.yml','.yaml','.csv') -and + $_.FullName -notmatch '[\\/]TestResults[\\/]' + } } It 'enumerates at least one target file' { diff --git a/AzLocal.UpdateManagement/Tests/test-run-timings.csv b/AzLocal.UpdateManagement/Tests/test-run-timings.csv index f20822b3..7692b2c2 100644 --- a/AzLocal.UpdateManagement/Tests/test-run-timings.csv +++ b/AzLocal.UpdateManagement/Tests/test-run-timings.csv @@ -5,3 +5,6 @@ TimestampUtc,ModuleVersion,Total,Passed,Failed,Skipped,WallClockSeconds,PesterDu "2026-06-11T14:43:38Z","0.8.71","1101","1099","1","1","128.88","127.79","8.92","chat-detached","5.1.26100.8655" "2026-06-11T14:46:29Z","0.8.71","1101","1100","0","1","89","87.71","2.27","chat-detached","5.1.26100.8655" "2026-06-11T17:21:55Z","0.8.72","1145","1106","0","1","96.82","88.13","8.67","chat-detached","5.1.26100.8655" +"2026-06-12T14:22:24Z","0.8.75","1182","1140","3","1","151.94","148.79","11.11","Invoke-Tests.ps1","5.1.26100.8655" +"2026-06-12T14:26:27Z","0.8.75","1182","1142","1","1","60.06","57.7","4","Invoke-Tests.ps1","5.1.26100.8655" +"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" diff --git a/AzLocal.UpdateManagement/docs/images/README.md b/AzLocal.UpdateManagement/docs/images/README.md index 84bfeffb..ea2296df 100644 --- a/AzLocal.UpdateManagement/docs/images/README.md +++ b/AzLocal.UpdateManagement/docs/images/README.md @@ -5,19 +5,28 @@ This folder holds the screenshots referenced from the module documentation. ## Convention - Filenames are **kebab-case**, descriptive of the surface they capture (workflow + view), e.g.: - - `apply-updates-summary.png` - the **Summary** tab of an `Step.6_apply-updates.yml` GitHub Actions run - - `auth-smoke-test-validate-oidc.png` - the **Validate OIDC + RBAC** job page of an `Step.0_authentication-test.yml` run + - `apply-updates-summary.png` - the **Summary** tab of an `apply-updates.yml` (Step.07) GitHub Actions run + - `auth-smoke-test-validate-oidc.png` - the **Validate OIDC + RBAC** job page of an `authentication-test.yml` (Step.00) run - **Prefer PNG**. Keep each file under ~250 KB; run through `pngquant` / `oxipng` before committing. - **No secrets**. Subscription IDs, tenant IDs, principal IDs, cluster GUIDs and cluster names must be masked or redacted in the captured frame before the screenshot lands here. The screenshots in this folder are taken from the public-safe `Azure/AzLocal.UpdateManagement` sandbox repo where those values are already replaced with `***`. - **Capture from the default GitHub dark theme** so the visual style stays consistent across the docs. -- Cap the total set at ~6-8 images. GitHub UI redesigns invalidate screenshots faster than text - keeping the set small reduces refresh effort. +- Cap the total set at ~8-10 images. GitHub UI redesigns invalidate screenshots faster than text - keeping the set small reduces refresh effort. ## Referenced by | File | Section | Image | |---|---|---| -| [`../../README.md`](../../README.md) | What's New in v0.7.60 | `apply-updates-summary.png` | +| [`../../README.md`](../../README.md) | What's New in v0.7.60 (archived in `docs/release-history.md`) | `apply-updates-summary.png` | +| [`../../README.md`](../../README.md) | What's New in v0.8.74 | `monitor-inflight-updates.png` | +| [`../../Automation-Pipeline-Examples/README.md`](../../Automation-Pipeline-Examples/README.md) | 1.1 Why the pipelines are named `Step.N - ` | `github-actions-10-pipelines-view.png` | | [`../../Automation-Pipeline-Examples/README.md`](../../Automation-Pipeline-Examples/README.md) | 5.1 GitHub Actions - auth smoke test | `auth-smoke-test-validate-oidc.png` | | [`../../Automation-Pipeline-Examples/README.md`](../../Automation-Pipeline-Examples/README.md) | 6.1 Inventory the estate | `inventory-clusters-run-output.png` | +| [`../../Automation-Pipeline-Examples/README.md`](../../Automation-Pipeline-Examples/README.md) | 6.7 Continuous fleet monitoring - monitor-updates | `monitor-inflight-updates.png` | +| [`../../Automation-Pipeline-Examples/README.md`](../../Automation-Pipeline-Examples/README.md) | 6.7 Continuous fleet monitoring - fleet-update-status | `fleet-update-status.png` | +| [`../../Automation-Pipeline-Examples/README.md`](../../Automation-Pipeline-Examples/README.md) | 6.7 Continuous fleet monitoring - fleet-health-status (failures by reason) | `fleet-health-status-part1.png` | +| [`../../Automation-Pipeline-Examples/README.md`](../../Automation-Pipeline-Examples/README.md) | 6.7 Continuous fleet monitoring - fleet-health-status (detailed results) | `fleet-health-status-part-2.png` | +| [`../../Automation-Pipeline-Examples/README.md`](../../Automation-Pipeline-Examples/README.md) | 8.3 Apply-Updates Schedule Coverage Audit - cron coverage remediation | `apply-updates-schedule-audit-part1.png` | +| [`../../Automation-Pipeline-Examples/README.md`](../../Automation-Pipeline-Examples/README.md) | 8.3 Apply-Updates Schedule Coverage Audit - NoWindowTag remediation | `apply-updates-schedule-audit-part2.png` | +| [`../../Automation-Pipeline-Examples/README.md`](../../Automation-Pipeline-Examples/README.md) | 8.3 Apply-Updates Schedule Coverage Audit - cycle calendar (enriched) | `apply-updates-schedule-audit-part3.png` | When adding or removing an image, update this table and the consuming markdown link in the same commit. diff --git a/AzLocal.UpdateManagement/docs/images/apply-updates-schedule-audit-part1.png b/AzLocal.UpdateManagement/docs/images/apply-updates-schedule-audit-part1.png new file mode 100644 index 00000000..c3ea7688 Binary files /dev/null and b/AzLocal.UpdateManagement/docs/images/apply-updates-schedule-audit-part1.png differ diff --git a/AzLocal.UpdateManagement/docs/images/apply-updates-schedule-audit-part2.png b/AzLocal.UpdateManagement/docs/images/apply-updates-schedule-audit-part2.png new file mode 100644 index 00000000..de85431c Binary files /dev/null and b/AzLocal.UpdateManagement/docs/images/apply-updates-schedule-audit-part2.png differ diff --git a/AzLocal.UpdateManagement/docs/images/apply-updates-schedule-audit-part3.png b/AzLocal.UpdateManagement/docs/images/apply-updates-schedule-audit-part3.png new file mode 100644 index 00000000..7f6383a6 Binary files /dev/null and b/AzLocal.UpdateManagement/docs/images/apply-updates-schedule-audit-part3.png differ diff --git a/AzLocal.UpdateManagement/docs/images/auth-smoke-test-validate-oidc.png b/AzLocal.UpdateManagement/docs/images/auth-smoke-test-validate-oidc.png index 1881b46e..8b1ee7b4 100644 Binary files a/AzLocal.UpdateManagement/docs/images/auth-smoke-test-validate-oidc.png and b/AzLocal.UpdateManagement/docs/images/auth-smoke-test-validate-oidc.png differ diff --git a/AzLocal.UpdateManagement/docs/images/fleet-health-status-part-2.png b/AzLocal.UpdateManagement/docs/images/fleet-health-status-part-2.png new file mode 100644 index 00000000..b15ec99e Binary files /dev/null and b/AzLocal.UpdateManagement/docs/images/fleet-health-status-part-2.png differ diff --git a/AzLocal.UpdateManagement/docs/images/fleet-health-status-part1.png b/AzLocal.UpdateManagement/docs/images/fleet-health-status-part1.png new file mode 100644 index 00000000..cd35bae5 Binary files /dev/null and b/AzLocal.UpdateManagement/docs/images/fleet-health-status-part1.png differ diff --git a/AzLocal.UpdateManagement/docs/images/fleet-update-status.png b/AzLocal.UpdateManagement/docs/images/fleet-update-status.png new file mode 100644 index 00000000..96e53fb2 Binary files /dev/null and b/AzLocal.UpdateManagement/docs/images/fleet-update-status.png differ diff --git a/AzLocal.UpdateManagement/docs/images/github-actions-10-pipelines-view.png b/AzLocal.UpdateManagement/docs/images/github-actions-10-pipelines-view.png new file mode 100644 index 00000000..21b2d244 Binary files /dev/null and b/AzLocal.UpdateManagement/docs/images/github-actions-10-pipelines-view.png differ diff --git a/AzLocal.UpdateManagement/docs/images/monitor-inflight-updates.png b/AzLocal.UpdateManagement/docs/images/monitor-inflight-updates.png new file mode 100644 index 00000000..6d50b8a3 Binary files /dev/null and b/AzLocal.UpdateManagement/docs/images/monitor-inflight-updates.png differ diff --git a/AzLocal.UpdateManagement/docs/release-history.md b/AzLocal.UpdateManagement/docs/release-history.md index 60c2078c..5a7ef3eb 100644 --- a/AzLocal.UpdateManagement/docs/release-history.md +++ b/AzLocal.UpdateManagement/docs/release-history.md @@ -4,7 +4,7 @@ > > **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.74 (the current release)**, see the main [README.md](../README.md#whats-new-in-v0874) `What's New in v0.8.74` section. +> **For v0.8.75 (the current release)**, see the main [README.md](../README.md#whats-new-in-v0875) `What's New in v0.8.75` section. ---