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 e8324605..bacf8feb 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.76' + GENERATED_AGAINST_MODULE_VERSION: '0.8.77' 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 27312c37..3053981c 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.76' + value: '0.8.77' # 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 05d7b4e8..12edf9b9 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.76' + GENERATED_AGAINST_MODULE_VERSION: '0.8.77' # 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 6465e6d3..d6c94c03 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.76' + value: '0.8.77' # 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 be1c7c1f..1dc7c7ba 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.76' + GENERATED_AGAINST_MODULE_VERSION: '0.8.77' # 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 c32897c4..56954c34 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.76' + GENERATED_AGAINST_MODULE_VERSION: '0.8.77' # 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 34eb7ec0..ada4b3dc 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.76' + GENERATED_AGAINST_MODULE_VERSION: '0.8.77' # 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 bd06e2c6..9f840c0e 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.76' + value: '0.8.77' # 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 c71a049b..9f37e9ac 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.76' + value: '0.8.77' # 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 7ca5358b..3a718513 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.76' + GENERATED_AGAINST_MODULE_VERSION: '0.8.77' 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 dcf7f48b..591ada71 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.76' + GENERATED_AGAINST_MODULE_VERSION: '0.8.77' # 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 1e4b08d8..cb47e0de 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.76' + GENERATED_AGAINST_MODULE_VERSION: '0.8.77' 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 ed7c5cf4..232c5b14 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.76' + GENERATED_AGAINST_MODULE_VERSION: '0.8.77' # 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 083eb953..dbcc2d45 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.76' + GENERATED_AGAINST_MODULE_VERSION: '0.8.77' # 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 aaf6c785..15c321e3 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.76' + GENERATED_AGAINST_MODULE_VERSION: '0.8.77' # 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 ee96aea3..d81284da 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.76' + GENERATED_AGAINST_MODULE_VERSION: '0.8.77' # 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 6cad12aa..22ebed5b 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.76' + GENERATED_AGAINST_MODULE_VERSION: '0.8.77' # 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 7f3f4c59..94613eb1 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 @@ -66,7 +66,7 @@ on: default: '' # --- ITSM Connector (ServiceNow auto-raise on fleet-update-status) --- # Set raise_itsm_ticket=true to open ServiceNow incidents from each unresolved - # Failed update run reported by the '📜 Update Run History and Error Details' + # Failed update run reported by the '📜 Update Run History and Error Details' # testsuite (and any failed AzureLocalFleetUpdateStatus row). Default is false # so existing schedules stay byte-identical until you opt in. The connector reads # the JUnit file Step.7 already produces (./reports/readiness-status.xml) and @@ -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.76' + GENERATED_AGAINST_MODULE_VERSION: '0.8.77' # 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 3de89674..001b773d 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.76' + GENERATED_AGAINST_MODULE_VERSION: '0.8.77' # 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 1416e113..98f55e66 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.76' + GENERATED_AGAINST_MODULE_VERSION: '0.8.77' # 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 bb220fe1..2f1d0e4a 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.76' + GENERATED_AGAINST_MODULE_VERSION: '0.8.77' # 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 efbfda40..d1e71f4b 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.76' + GENERATED_AGAINST_MODULE_VERSION: '0.8.77' # 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 b13e0359..365e66a7 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.76' + ModuleVersion = '0.8.77' # Supported PSEditions CompatiblePSEditions = @('Desktop', 'Core') @@ -309,13 +309,14 @@ # ReleaseNotes of this module ReleaseNotes = @' -## 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 (master gate `SIDELOAD_UPDATES` not set, or set + no self-hosted `azlocal-sideload` runner registered) produced `Status: Skipped` with no logs, no annotation, and no actionable feedback - operators had to read the YAML to understand why. 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 explaining what is set, what is missing, and how to enable Step.6. Also broadens the master gate to accept `'true'`, `'True'`, `'TRUE'`, or `'1'` (was strict-literal `'true'` only). No public API change or new exports (still 60). +## 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'`. -- **NEW (GitHub Actions `sideload-updates.yml`)**: `preflight` job on `windows-latest` with `permissions: actions: read, contents: read`. Outcomes: gate OFF -> succeeds (`::notice`) with full enablement walkthrough in step summary; gate ON + `SIDELOAD_STATE_ROOT` missing -> fails (`::error`, exit 1) with missing-variable panel; gate ON + best-effort runner enumeration finds no online `azlocal-sideload` runner -> fails with no-online-runner panel; gate ON + runner API returns 403/404 (typical, `GITHUB_TOKEN` lacks admin) -> succeeds with manual-verification warning; all OK -> succeeds with "Preflight passed" panel. The `sideload` job gains `needs: preflight` so it cannot queue indefinitely when prerequisites are missing. -- **NEW (Azure DevOps `sideload-updates.yml`)**: `Preflight` stage on `pool: vmImage: windows-latest`. Validates the gate + `SIDELOAD_STATE_ROOT`; uses `##vso[task.uploadsummary]` to render the markdown panel. Does NOT enumerate ADO agents (the `Agent Pools (read)` scope is not normally granted to the pipeline identity). The `Sideload` stage gains `dependsOn: Preflight`. -- **CHANGE (master gate)**: both `sideload-updates.yml` files now accept `SIDELOAD_UPDATES` in `'true'` / `'True'` / `'TRUE'` / `'1'`. GH uses `contains(fromJSON(...), vars.SIDELOAD_UPDATES)`; ADO uses an `or(eq..., eq...)` condition. Operators copying the README still get `'true'` as the canonical value; the additional accepted values are forgiving of case habits. -- **DOCS**: `Automation-Pipeline-Examples/docs/sideload.md` updated - section 6 gate row reflects the four accepted values, header note documents the preflight, new section 9 "Preflight (v0.8.76+)" with full behaviour matrix + rationale for why a Microsoft-hosted Windows runner is the right host for preflight (no fabric / Key Vault access needed). -- **All bundled pipeline templates** bump `GENERATED_AGAINST_MODULE_VERSION` from `'0.8.75'` to `'0.8.76'`. +- **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`. +- **FIX (`Test-AzLocalClusterHealth` - Step.05/06 readiness)**: 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 bug was present in `Get-HealthCheckFailureSummary` (Private) and `Get-AzLocalFleetStatusData` (Public, `-IncludeHealthDetails` path). All three sites now use the `PSObject.Properties[...]` guard idiom; `Test-AzLocalClusterHealth` correctly classifies such clusters as `HealthState="No Data" / Passed=$true` and continues. +- **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. @@ -335,14 +336,6 @@ ## 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. -## Version 0.8.7 - On-prem solution-update sideloading automation (new Step.6 pipeline + 5 new Public cmdlets) + pipeline filenames de-numbered (rename-aware Update) + breaking display-step renumber. Module export count grows 55 -> 60. See CHANGELOG for the full v0.8.7 entry. - -## Version 0.8.6 - Step.3 cycle-calendar enrichment (per-day CRON + UpdateStartWindow tag coverage) + six v0.8.5 pipeline regression fixes (Step.0/3/4/6/9) + new Pester static-audit guards. See CHANGELOG for the full v0.8.6 entry. - -## Version 0.8.5 - New Public cmdlet `Get-AzLocalApplyUpdatesScheduleCycleCalendar` + Step.6 manual schedule-file inputs + Step.3 cycle-calendar regression fix + per-ring cluster-count column + full thin-YAML port of all 10 Step pipelines (14 new Public cmdlets). Module export count grows 35 -> 55. See CHANGELOG for the full v0.8.5 entry. - -## Version 0.7.99 - Property/Summary renames (AvailableUpdates -> AllAvailableUpdates, AvailableUpdatesCount -> ActionableUpdatesCount, Ready/NotReady Summary -> ReadyForUpdate/UpToDate/NotReadyForUpdate) + Step.7 CRITICAL elapsed-days 7->3 + artifact zip names prefixed with step.X- - For full v0.7.x and v0.8.x release notes see: https://github.com/NeilBird/Azure-Local/blob/main/AzLocal.UpdateManagement/CHANGELOG.md diff --git a/AzLocal.UpdateManagement/AzLocal.UpdateManagement.psm1 b/AzLocal.UpdateManagement/AzLocal.UpdateManagement.psm1 index 8a71b879..82e6eba2 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.76' +$script:ModuleVersion = '0.8.77' $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 c694dd66..c7cd1a5a 100644 --- a/AzLocal.UpdateManagement/CHANGELOG.md +++ b/AzLocal.UpdateManagement/CHANGELOG.md @@ -5,6 +5,26 @@ 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.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'`. + +### Fixed + +- **`Start-AzLocalClusterUpdate` (Step.07 main entry) - clusters without an `UpdateStartWindow` tag.** 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 guard `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`. +- **`Test-AzLocalClusterHealth` (Step.05/06 readiness gate) - clusters with no `properties.healthCheckResult` field.** 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 by the update-health pipeline). 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 (observed on Arizona and Sydney during the 2026-06-10..2026-06-14 window). The same bare-access pattern was present in `Get-HealthCheckFailureSummary` (Private helper) and `Get-AzLocalFleetStatusData` (Public, `-IncludeHealthDetails` path). All three sites now use the `PSObject.Properties[...]` guard idiom; `Test-AzLocalClusterHealth` correctly classifies such clusters as `HealthState="No Data" / Passed=$true` and continues. + +### Tests + +- Two new Pester regression contexts: + - `v0.8.77 strict-mode regression: missing UpdateStartWindow tag` on `Start-AzLocalClusterUpdate` - feeds a `PSCustomObject` tag bag that deliberately omits the window tags, asserts `Should -Not -Throw`, and verifies `Test-AzLocalUpdateScheduleAllowed` is **not** called (proving the "no window tag = any time" semantic). + - `v0.8.77 strict-mode regression: missing properties.healthCheckResult` on `Test-AzLocalClusterHealth` - feeds an ARG-shaped summary row whose `properties` PSCustomObject omits `healthCheckResult`, asserts `Should -Not -Throw`, and verifies the result row classifies as `HealthState='No Data'` / `Passed=$true`. +- Both bugs were **invisible** to parse-time analysis (`Get-CmdletErrors` / get_errors / lint) - only runtime against the property-less shape triggers the strict-mode throw. The new tests use the minimal real-world fixtures that match the production shapes. + +### Pipeline templates + +- All bundled pipeline templates bump `GENERATED_AGAINST_MODULE_VERSION` from `'0.8.76'` to `'0.8.77'`. The pipeline YAML bodies themselves are unchanged. + ## [0.8.76] - 2026-06-12 Patch release. 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 an operator who triggers Step.6 without first completing the opt-in setup sees a clear "what to do next" panel in the run step summary instead of a silent `Status: Skipped`. Also broadens the master gate to accept `'true'`, `'True'`, `'TRUE'`, or `'1'` (was strict-literal `'true'` only). diff --git a/AzLocal.UpdateManagement/Private/Get-HealthCheckFailureSummary.ps1 b/AzLocal.UpdateManagement/Private/Get-HealthCheckFailureSummary.ps1 index e92d28fb..67b066d5 100644 --- a/AzLocal.UpdateManagement/Private/Get-HealthCheckFailureSummary.ps1 +++ b/AzLocal.UpdateManagement/Private/Get-HealthCheckFailureSummary.ps1 @@ -13,7 +13,11 @@ function Get-HealthCheckFailureSummary { [object]$UpdateSummary ) - if (-not $UpdateSummary -or -not $UpdateSummary.properties.healthCheckResult) { + # v0.8.77: guard property reads - bare `$UpdateSummary.properties.healthCheckResult` + # THROWS under Set-StrictMode -Version Latest when the property is absent. + $summaryProps = if ($UpdateSummary) { $UpdateSummary.PSObject.Properties['properties'] } else { $null } + $hasHCR = $summaryProps -and $UpdateSummary.properties.PSObject.Properties['healthCheckResult'] -and $UpdateSummary.properties.healthCheckResult + if (-not $hasHCR) { return "" } diff --git a/AzLocal.UpdateManagement/Public/Get-AzLocalFleetStatusData.ps1 b/AzLocal.UpdateManagement/Public/Get-AzLocalFleetStatusData.ps1 index 03017258..53c47ac4 100644 --- a/AzLocal.UpdateManagement/Public/Get-AzLocalFleetStatusData.ps1 +++ b/AzLocal.UpdateManagement/Public/Get-AzLocalFleetStatusData.ps1 @@ -465,7 +465,10 @@ function Get-AzLocalFleetStatusData { # Health details (from already-fetched update summary) if ($IncludeHealthDetails) { $failures = @() - if ($hasSummary -and $updateSummary.properties.healthCheckResult) { + # v0.8.77: guard property reads under Set-StrictMode -Version Latest. + $hcrProps = if ($hasSummary -and $updateSummary) { $updateSummary.PSObject.Properties['properties'] } else { $null } + $hasHCR = $hcrProps -and $updateSummary.properties.PSObject.Properties['healthCheckResult'] -and $updateSummary.properties.healthCheckResult + if ($hasHCR) { foreach ($check in $updateSummary.properties.healthCheckResult) { if ($check.status -eq "Failed") { $sev = if ($check.severity) { $check.severity } else { "Unknown" } diff --git a/AzLocal.UpdateManagement/Public/Start-AzLocalClusterUpdate.ps1 b/AzLocal.UpdateManagement/Public/Start-AzLocalClusterUpdate.ps1 index c3817296..a7ae93bc 100644 --- a/AzLocal.UpdateManagement/Public/Start-AzLocalClusterUpdate.ps1 +++ b/AzLocal.UpdateManagement/Public/Start-AzLocalClusterUpdate.ps1 @@ -795,10 +795,24 @@ function Start-AzLocalClusterUpdate { } # Step 3c: Schedule/maintenance window validation - # Check UpdateStartWindow and UpdateExclusionsWindow tags if present on the cluster resource - $clusterTags = $clusterInfo.tags - $windowTagValue = if ($clusterTags -and $clusterTags.$($script:UpdateStartWindowTagName)) { $clusterTags.$($script:UpdateStartWindowTagName) } else { $null } - $exclusionTagValue = if ($clusterTags -and $clusterTags.$($script:UpdateExclusionsWindowTagName)) { $clusterTags.$($script:UpdateExclusionsWindowTagName) } else { $null } + # Check UpdateStartWindow and UpdateExclusionsWindow tags if present on the cluster resource. + # v0.8.77: tag absence MUST be treated as "no window restriction" (any time eligible). + # Bare $clusterTags. reads THROW under Set-StrictMode -Version Latest when the + # property is absent on a PSCustomObject (clusterInfo.tags from Invoke-AzRestJson). + # Guard with PSObject.Properties so missing tags resolve to $null rather than blowing up + # the entire Start-AzLocalClusterUpdate call for that cluster. + $clusterTags = $clusterInfo.tags + $windowTagValue = $null + $exclusionTagValue = $null + if ($clusterTags) { + if ($clusterTags -is [System.Collections.IDictionary]) { + if ($clusterTags.Contains($script:UpdateStartWindowTagName)) { $windowTagValue = $clusterTags[$script:UpdateStartWindowTagName] } + if ($clusterTags.Contains($script:UpdateExclusionsWindowTagName)) { $exclusionTagValue = $clusterTags[$script:UpdateExclusionsWindowTagName] } + } else { + if ($clusterTags.PSObject.Properties[$script:UpdateStartWindowTagName]) { $windowTagValue = $clusterTags.$($script:UpdateStartWindowTagName) } + if ($clusterTags.PSObject.Properties[$script:UpdateExclusionsWindowTagName]) { $exclusionTagValue = $clusterTags.$($script:UpdateExclusionsWindowTagName) } + } + } if ($windowTagValue -or $exclusionTagValue) { Write-Log -Message "Step 3c: Checking maintenance schedule tags..." -Level Info diff --git a/AzLocal.UpdateManagement/Public/Test-AzLocalClusterHealth.ps1 b/AzLocal.UpdateManagement/Public/Test-AzLocalClusterHealth.ps1 index 40a2706b..06ea1400 100644 --- a/AzLocal.UpdateManagement/Public/Test-AzLocalClusterHealth.ps1 +++ b/AzLocal.UpdateManagement/Public/Test-AzLocalClusterHealth.ps1 @@ -248,7 +248,19 @@ function Test-AzLocalClusterHealth { $key = $cluster.ResourceId.ToLower() $summary = if ($summaryByCluster.ContainsKey($key)) { $summaryByCluster[$key] } else { $null } - if (-not $summary -or -not $summary.properties.healthCheckResult) { + # v0.8.77: guard property reads with PSObject.Properties because + # `$summary.properties.healthCheckResult` (bare access) THROWS under + # Set-StrictMode -Version Latest when `healthCheckResult` is absent + # on the `properties` PSCustomObject. Clusters whose ARM update + # summary genuinely has no health-check data (typical for clusters + # that have not had an update-readiness probe yet) were producing + # `Checking: ... Error: The property 'healthCheckResult' + # cannot be found on this object.` in Step.05/06/07 production runs, + # which then poisoned the readiness gate (Passed=$false flagged the + # cluster as blocked when it was simply un-probed). + $summaryProps = if ($summary) { $summary.PSObject.Properties['properties'] } else { $null } + $hasHCR = $summaryProps -and $summary.properties.PSObject.Properties['healthCheckResult'] -and $summary.properties.healthCheckResult + if (-not $hasHCR) { Write-Host " No Health Data" -ForegroundColor Yellow $results += [PSCustomObject]@{ ClusterName = $clusterName; HealthState = "No Data"; Passed = $true @@ -257,7 +269,7 @@ function Test-AzLocalClusterHealth { continue } - $healthState = if ($summary.properties.healthState) { [string]$summary.properties.healthState } else { "Unknown" } + $healthState = if ($summary.properties.PSObject.Properties['healthState'] -and $summary.properties.healthState) { [string]$summary.properties.healthState } else { "Unknown" } $healthChecks = $summary.properties.healthCheckResult # Extract failures (Critical and Warning only; use -BlockingOnly for Critical only) diff --git a/AzLocal.UpdateManagement/README.md b/AzLocal.UpdateManagement/README.md index 8ee64875..5339cb2c 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.76 - [Published in PowerShell Gallery](https://www.powershellgallery.com/packages/AzLocal.UpdateManagement/0.8.76) +**Latest Version:** v0.8.77 - [Published in PowerShell Gallery](https://www.powershellgallery.com/packages/AzLocal.UpdateManagement/0.8.77) > 📢 **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.76](#whats-new-in-v0876) +- [What's New in v0.8.77](#whats-new-in-v0877) - [Files](#files) - [Prerequisites](#prerequisites) - [RBAC Requirements](#rbac-requirements) (summary; full reference in [docs/rbac.md](docs/rbac.md)) @@ -86,18 +86,17 @@ 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.76 +## What's New in v0.8.77 -**Patch release. Adds a Microsoft-hosted Windows preflight job (GitHub Actions) / preflight stage (Azure DevOps) in front of the opt-in Step.6 `sideload-updates.yml` pipeline.** Before v0.8.76, triggering Step.6 without first completing the opt-in setup (master gate `SIDELOAD_UPDATES` not set, or set without registering a self-hosted `azlocal-sideload` runner) produced `Status: Skipped` with no logs, no annotation, and no actionable feedback - operators had to read the YAML to figure out why. 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 explaining what is set, what is missing, and how to enable Step.6. Also broadens the master gate to accept `'true'`, `'True'`, `'TRUE'`, or `'1'` (was strict-literal `'true'` only). No public API change or new exports (still 60). +**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`. -1. **Added (GitHub Actions)**: new `preflight` job in `sideload-updates.yml` on `runs-on: windows-latest` with `permissions: actions: read, contents: read`. The job ALWAYS runs (no `if:` gate). Outcomes: gate OFF -> succeeds with a `::notice` annotation and a full enablement walkthrough in the step summary; gate ON + `SIDELOAD_STATE_ROOT` missing -> fails (`::error`, `exit 1`) with a missing-variable panel; gate ON + best-effort `/repos/{owner}/{repo}/actions/runners` enumeration finds no online `azlocal-sideload` runner -> fails with a no-online-runner panel; gate ON + the API returns 403/404 (the typical case - `GITHUB_TOKEN` lacks admin) -> succeeds with a manual-verification warning panel; all OK -> succeeds with a "Preflight passed" panel. The `sideload` job gains `needs: preflight` so it cannot queue indefinitely when prerequisites are missing. -2. **Added (Azure DevOps)**: new `Preflight` stage in `sideload-updates.yml` on `pool: vmImage: windows-latest`. Validates the gate + `SIDELOAD_STATE_ROOT`; uploads the markdown panel via `##vso[task.uploadsummary]`. Does NOT enumerate ADO agents (the `Agent Pools (read)` scope is not normally granted to the pipeline identity, so the preflight degrades to documentation in that case). The `Sideload` stage gains `dependsOn: Preflight`. -3. **Changed (master gate broadened)**: both `sideload-updates.yml` files now accept `SIDELOAD_UPDATES` in `'true'` / `'True'` / `'TRUE'` / `'1'`. GH uses `contains(fromJSON('["true","True","TRUE","1"]'), vars.SIDELOAD_UPDATES)`; ADO uses an `or(eq..., eq...)` condition. The README still recommends `'true'` as the canonical value; the additional accepted values are forgiving of case-insensitive habits. -4. **Docs**: `Automation-Pipeline-Examples/docs/sideload.md` updated - section 6 gate row reflects the four accepted values, header note documents the preflight, new section 9 "Preflight (v0.8.76+)" with the full behaviour matrix and a short rationale for why a Microsoft-hosted Windows runner is the correct host for preflight (no fabric / Key Vault access required). +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. -`GENERATED_AGAINST_MODULE_VERSION` bumped from `0.8.75` to `0.8.76` across all bundled pipeline templates. +`GENERATED_AGAINST_MODULE_VERSION` bumped from `0.8.76` to `0.8.77` across all bundled pipeline templates. -See [CHANGELOG.md](CHANGELOG.md#0876---2026-06-12) for the full v0.8.76 entry. See [`What's New in v0.8.75`](docs/release-history.md#whats-new-in-v0875) in the Release History for the previous release. +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. ## Files @@ -576,7 +575,13 @@ 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.76** stay above under [`What's New in v0.8.76`](#whats-new-in-v0876). +The most recent release notes for **v0.8.77** stay above under [`What's New in v0.8.77`](#whats-new-in-v0877). + +### What's New in v0.8.76 + +**Patch release. Adds a Microsoft-hosted Windows preflight job (GitHub Actions) / preflight stage (Azure DevOps) in front of the opt-in Step.6 `sideload-updates.yml` pipeline.** Before v0.8.76, triggering Step.6 without first completing the opt-in setup (master gate `SIDELOAD_UPDATES` not set, or set without registering a self-hosted `azlocal-sideload` runner) produced `Status: Skipped` with no logs, no annotation, and no actionable feedback - operators had to read the YAML to figure out why. 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 explaining what is set, what is missing, and how to enable Step.6. Also broadens the master gate to accept `'true'`, `'True'`, `'TRUE'`, or `'1'` (was strict-literal `'true'` only). No public API change or new exports (still 60). + +See [CHANGELOG.md](CHANGELOG.md#0876---2026-06-12) for the full v0.8.76 entry. ### 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 8492f2d3..198a98fc 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.76' { - $script:ModuleInfo.Version | Should -Be '0.8.76' + It 'Should have version 0.8.77' { + $script:ModuleInfo.Version | Should -Be '0.8.77' } It 'Module version constants are in sync between .psm1 and .psd1' { @@ -3477,6 +3477,53 @@ Describe 'Function: Test-AzLocalClusterHealth' { $noun | Should -BeLike 'AzLocal*' } } + + Context 'v0.8.77 strict-mode regression: missing properties.healthCheckResult' { + # Production Step.05/06 (Azure/AzLocal.UpdateManagement) 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 that had not been probed yet). Bare + # $summary.properties.healthCheckResult under Set-StrictMode -Version + # Latest THROWS, the catch block flagged the cluster + # HealthState=Error / Passed=$false, and the readiness gate then + # falsely blocked Arizona / Sydney from updates. Fix uses + # PSObject.Properties[] indexing to guard the property read so the + # function returns 'No Data' / Passed=$true instead of throwing. + It 'Should classify a cluster with no healthCheckResult as "No Data" (Passed=$true) and NOT throw' { + InModuleScope AzLocal.UpdateManagement { + $resourceId = '/subscriptions/s/resourceGroups/r/providers/Microsoft.AzureStackHCI/clusters/no-hcr' + + # Summary row mirrors the ARG `updatesummaries` projection + # shape Test-AzLocalClusterHealth consumes (id / name / + # properties / ClusterResourceId_). DELIBERATELY omits the + # `healthCheckResult` property from `properties`. + $summaryRow = [PSCustomObject]@{ + id = "$resourceId/updateSummaries/default" + name = 'default' + properties = [PSCustomObject]@{ state = 'AppliedSuccessfully' } + ClusterResourceId_ = $resourceId.ToLower() + } + + function global:az { $global:LASTEXITCODE = 0; return '{}' } + Mock Test-AzCliAvailable { return $true } + Mock Install-AzGraphExtension { return $true } + Mock Invoke-AzResourceGraphQuery { return @($summaryRow) } + + $result = $null + # Note: assignment inside the Should -Not -Throw scriptblock + # runs in its own scope and does NOT propagate; call once for + # the throw assertion then capture again for property asserts. + { Test-AzLocalClusterHealth ` + -ClusterResourceIds @($resourceId) -PassThru 6>$null | Out-Null } | Should -Not -Throw + $result = Test-AzLocalClusterHealth -ClusterResourceIds @($resourceId) -PassThru 6>$null + + $result | Should -Not -BeNullOrEmpty + $result.HealthState | Should -Be 'No Data' + $result.Passed | Should -Be $true + } + } + } } #endregion Pre-Update Health Validation Tests @@ -5098,6 +5145,88 @@ Describe 'Start-AzLocalClusterUpdate (prefetched pass-through)' { } } } + + Context 'v0.8.77 strict-mode regression: missing UpdateStartWindow tag' { + # Production Step.07 (Azure/AzLocal.UpdateManagement) emitted: "Error + # processing cluster 'Portland': The property 'UpdateStartWindow' cannot + # be found on this object." for clusters whose tag bag did not include + # UpdateStartWindow / UpdateExclusionsWindow. Bare $clusterTags. + # reads throw under Set-StrictMode -Version Latest when the property is + # absent on a PSCustomObject. The intent is "no tag = any time + # eligible" (no window restriction), matching the schedule audit's + # NoWindowTag advisory-not-blocking design. + It 'Should NOT throw when cluster tags object lacks UpdateStartWindow and UpdateExclusionsWindow' { + InModuleScope AzLocal.UpdateManagement { + $resourceId = '/subscriptions/s/resourceGroups/r/providers/Microsoft.AzureStackHCI/clusters/no-window-tag' + + # tags is a real PSCustomObject (matches Invoke-AzRestJson shape) + # but deliberately omits UpdateStartWindow / UpdateExclusionsWindow. + $tagsWithoutWindow = [PSCustomObject]@{ Environment = 'Production'; UpdateRing = 'Stable' } + + Mock Test-ExportPathWritable { return $true } + Mock Test-AzCliAvailable { return $true } + Mock Get-AzLocalClusterInfo { + return [PSCustomObject]@{ + id = $resourceId + name = 'no-window-tag' + tags = $tagsWithoutWindow + properties = [PSCustomObject]@{ status = 'ConnectedRecently' } + } + } + # Stop early with a state that produces a Skipped record so the + # function exits cleanly after the schedule-tag read code path. + Mock Get-AzLocalUpdateSummary { + return [PSCustomObject]@{ + properties = [PSCustomObject]@{ + state = 'NotApplicableForTest' + healthState = 'Success' + } + } + } + + { Start-AzLocalClusterUpdate ` + -ClusterResourceIds @($resourceId) ` + -Force ` + -PassThru 4>$null 6>$null | Out-Null } | Should -Not -Throw + } + } + + It 'Should treat missing UpdateStartWindow as "any time" and proceed past the schedule gate' { + InModuleScope AzLocal.UpdateManagement { + $resourceId = '/subscriptions/s/resourceGroups/r/providers/Microsoft.AzureStackHCI/clusters/any-time' + + # No tags at all on the cluster - schedule gate should be a no-op, + # and Test-AzLocalUpdateScheduleAllowed should NOT be invoked + # because $windowTagValue and $exclusionTagValue are both $null. + Mock Test-ExportPathWritable { return $true } + Mock Test-AzCliAvailable { return $true } + Mock Get-AzLocalClusterInfo { + return [PSCustomObject]@{ + id = $resourceId + name = 'any-time' + tags = [PSCustomObject]@{} + properties = [PSCustomObject]@{ status = 'ConnectedRecently' } + } + } + Mock Get-AzLocalUpdateSummary { + return [PSCustomObject]@{ + properties = [PSCustomObject]@{ + state = 'NotApplicableForTest' + healthState = 'Success' + } + } + } + Mock Test-AzLocalUpdateScheduleAllowed { throw 'schedule gate must not be called when no window tags are set' } + + $null = Start-AzLocalClusterUpdate ` + -ClusterResourceIds @($resourceId) ` + -Force ` + -PassThru 4>$null 6>$null + + Assert-MockCalled Test-AzLocalUpdateScheduleAllowed -Times 0 -Exactly + } + } + } } #endregion Integration: Start-AzLocalClusterUpdate prefetched pass-through diff --git a/AzLocal.UpdateManagement/docs/release-history.md b/AzLocal.UpdateManagement/docs/release-history.md index 81b8b8f0..f7ee7b58 100644 --- a/AzLocal.UpdateManagement/docs/release-history.md +++ b/AzLocal.UpdateManagement/docs/release-history.md @@ -4,7 +4,17 @@ > > **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.76 (the current release)**, see the main [README.md](../README.md#whats-new-in-v0876) `What's New in v0.8.76` section. +> **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. + +--- + +### What's New in v0.8.76 + +v0.8.76 adds a Microsoft-hosted Windows preflight job (GitHub Actions) / preflight stage (Azure DevOps) in front of the opt-in Step.6 `sideload-updates.yml` pipeline. Before v0.8.76, triggering Step.6 without first completing the opt-in setup (master gate `SIDELOAD_UPDATES` not set, or set without registering a self-hosted `azlocal-sideload` runner) produced `Status: Skipped` with no logs, no annotation, and no actionable feedback. The new `preflight` job (`runs-on: windows-latest`, ~10s, no Azure access) ALWAYS runs and writes a clear panel to the run step summary. Master gate broadened to accept `'true'` / `'True'` / `'TRUE'` / `'1'`. No public API change or new exports (still 60). + +`GENERATED_AGAINST_MODULE_VERSION` bumped from `0.8.75` to `0.8.76` across all bundled pipeline templates. + +See [CHANGELOG.md](../CHANGELOG.md#0876---2026-06-12) for the full v0.8.76 entry. ---