diff --git a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/README.md b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/README.md index dea2ac14..71f6417a 100644 --- a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/README.md +++ b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/README.md @@ -64,7 +64,7 @@ By the end of this guide you will have: - **Fleet Connectivity Status** (Step.4, v0.7.79+, enhanced in v0.7.85) - read-only daily snapshot of Arc agent connectivity, physical NIC health, Azure Resource Bridge status, and the node-count reconciliation between cluster `reportedProperties.nodes` and Arc-tagged physical machines. *Scheduled daily 05:30 UTC + manual.* - **Assess Update Readiness** (Step.5) - pre-flight, report-only readiness + blocking-health snapshot, published as JUnit XML. *Manual only.* - **Sideload Updates** (Step.6, v0.8.7) - **opt-in, off by default, on-prem self-hosted runner/agent required.** Pre-stages solution-update media onto clusters that cannot pull updates from Azure directly (dark / air-gapped fabrics): Robocopy to the cluster import share, remote SHA256 verify, `Add-SolutionUpdate` import, then flip `UpdateSideloaded=True` for the downstream Step.7 apply. Re-entrant state machine driven by a frequent CRON; the multi-hour copy runs in a detached Scheduled Task. *Inert unless `SIDELOAD_UPDATES=true`. See [sideload.md](docs/sideload.md) and [sideload-robocopy.md](docs/sideload-robocopy.md).* - - **Apply Updates** (Step.7; **v0.8.78 step-summary UX polish**: ``ScheduleBlocked`` / ``SideloadedBlocked`` / ``ExcludedByTag`` outcomes render as JUnit ```` instead of ```` so ``dorny/test-reporter`` no longer flips Step.07 RED on by-design gate-respect outcomes - ``HealthCheckBlocked`` deliberately stays a ````; the Readiness KPI table now also surfaces **Already Up to Date** and **Not Ready (needs attention before updating)** rows so operators see the full ring breakdown - eligible, started, skipped, blocked, up-to-date, needs-attention - in one place; GHA-only: ``actions/download-artifact@v6 -> @v7`` silences the Node 20 deprecation warning) - apply updates to a single `UpdateRing` wave at a time, with WhatIf / dry-run support. *Manual only by default - **you must add a schedule** that lines up with your cluster `UpdateStartWindow` tags, see [Step 7 - Apply Updates](docs/appendix-pipelines.md#step-7---apply-updates) and [section 8](#8-scheduling-maintenance-windows-and-change-freeze-periods).* + - **Apply Updates** (Step.7; **v0.8.79 break-glass override**: optional `force_immediate_update` / `forceImmediateUpdate` pipeline parameter (defaults `false`, **manual run / queue only**) bypasses the per-cluster `UpdateStartWindow` / `UpdateExclusionsWindow` maintenance-window gate for emergency / out-of-window patching - the override is unreachable from the scheduled `apply-updates-schedule.yml` configuration file and a high-visibility WARNING banner is emitted into the run log when it fires; **v0.8.78 step-summary UX polish**: ``ScheduleBlocked`` / ``SideloadedBlocked`` / ``ExcludedByTag`` outcomes render as JUnit ```` instead of ```` so ``dorny/test-reporter`` no longer flips Step.07 RED on by-design gate-respect outcomes - ``HealthCheckBlocked`` deliberately stays a ````; the Readiness KPI table now also surfaces **Already Up to Date** and **Not Ready (needs attention before updating)** rows so operators see the full ring breakdown - eligible, started, skipped, blocked, up-to-date, needs-attention - in one place; GHA-only: ``actions/download-artifact@v6 -> @v7`` silences the Node 20 deprecation warning) - apply updates to a single `UpdateRing` wave at a time, with WhatIf / dry-run support. *Manual only by default - **you must add a schedule** that lines up with your cluster `UpdateStartWindow` tags, see [Step 7 - Apply Updates](docs/appendix-pipelines.md#step-7---apply-updates) and [section 8](#8-scheduling-maintenance-windows-and-change-freeze-periods).* - **Monitor In-Flight Updates** (Step.8, v0.7.90; v0.7.96 surfaces `Status` + deepest `ErrorMessage` columns, adds the `StepError` stuck-step JUnit type for runs that have hit an error inside a step without crossing the long-running threshold, and renders portal-linked Cluster Name / Update Name cells in the markdown summary; **v0.7.98 UX overhaul**: composite `SeverityScore` sort, per-cell `StateIcon` + `StatusIcon`, horizontal chip stack (`STEP-STUCK` / `RUN-STUCK` / `UNRESOLVED` / `RECENT-FAIL`), `CRITICAL / WARN / OK` fleet status badge at the top of the job summary, collapsible `
` Verbose Error block per row, and JUnit `` + `` populated with real run elapsed seconds). Operational snapshot during an active wave: lists each cluster whose latest update run is `InProgress`, with current step, progress (`completed/total steps`), elapsed duration, the `Status` column (`Success`/`Error`/`InProgress`/...), and the deepest `errorMessage` walked out of the nested ARM `steps[]` tree; flags long-running runs (default >6h) AND step-errored stuck runs as JUnit failures in the Checks tab. *Scheduled 5x/day at 20:00, 22:00, 00:00, 02:00, 04:00 UTC (every 2h across the typical overnight maintenance window) + manual; default cadence is editable in `monitor-updates.yml` (v0.7.92+).* - **Fleet Update Status** (Step.9, formerly Step.8; v0.7.96 promotes `NeedsAttention` into the **Update Failed** bucket, adds a new **Action Required** bucket for `PreparationFailed`, and folds `PreparationInProgress` into **Update In Progress**; **v0.7.98** populates JUnit `time=` on each `` in the `📜 Update Run History and Error Details` testsuite using `DurationMinutes * 60`). Scheduled daily snapshot of fleet update state, surfaced in the Tests tab. Markdown summary's `📜 Update Run History and Error Details` table now also carries portal-linked Cluster Name / Update Name cells plus the deepest-step `ErrorMessage`. *Scheduled daily 06:00 UTC + manual.* - **Fleet Health Status** (Step.10, formerly Step.9) - scheduled daily snapshot of 24-hour system health-check failures, surfaced in the Tests tab. *Scheduled daily 07:00 UTC + manual.* diff --git a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/apply-updates-schedule-audit.yml b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/apply-updates-schedule-audit.yml index 694d3d47..5d058798 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.78' + GENERATED_AGAINST_MODULE_VERSION: '0.8.79' 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 12af9a5e..34eaf84f 100644 --- a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/apply-updates.yml +++ b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/azure-devops/apply-updates.yml @@ -85,6 +85,15 @@ parameters: type: boolean default: true + # v0.8.79 break-glass override. Only honoured when $(Build.Reason) -eq 'Manual' + # (operator-initiated queue from the Azure DevOps UI / CLI). Schedule-triggered + # runs deliberately ignore this flag even if a YAML edit ever sets the default + # to true - the apply step rechecks $(Build.Reason) at runtime. + - name: forceImmediateUpdate + displayName: 'WARNING: FORCE IMMEDIATE UPDATE - bypass UpdateStartWindow / UpdateExclusionsWindow tags and apply updates immediately. Break-glass override for emergency / out-of-window patching. MANUAL QUEUE ONLY - schedule-triggered runs ignore this flag.' + type: boolean + default: false + # --- ITSM Connector (ServiceNow) --- # Opt-in ticket creation. Defaults preserve the previous pipeline behaviour # (no ITSM calls). Secrets are wired via the variable group referenced below @@ -120,7 +129,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.78' + value: '0.8.79' # 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). @@ -366,6 +375,11 @@ stages: # clusters with no matching Ready update are skipped with # status 'NotInAllowList'. RESOLVED_ALLOWED_UPDATE_VERSIONS: $(resolvedAllowedUpdateVersions) + # v0.8.79 break-glass override - parameter value + Build.Reason guard. + # The runtime check below collapses force=true on Schedule/CI runs to + # force=false so the override only ever takes effect on a Manual queue. + FORCE_IMMEDIATE_UPDATE_PARAM: ${{ parameters.forceImmediateUpdate }} + BUILD_REASON: $(Build.Reason) inputs: azureSubscription: 'AzureLocal-ServiceConnection' # Update with your service connection name scriptType: 'pscore' @@ -380,6 +394,14 @@ stages: OutputDirectory = "$(Build.ArtifactStagingDirectory)" } if ([System.Convert]::ToBoolean("${{ parameters.dryRun }}")) { $params['DryRun'] = $true } + if ($env:FORCE_IMMEDIATE_UPDATE_PARAM -eq 'True') { + if ($env:BUILD_REASON -eq 'Manual') { + $params['ForceImmediateUpdate'] = $true + } + else { + Write-Host "##vso[task.logissue type=warning]forceImmediateUpdate=True ignored: Build.Reason='$env:BUILD_REASON' (override is honoured only for Manual queue runs)." + } + } Invoke-AzLocalReadinessGatedClusterUpdate @params # compute a UTC timestamp variable so both update-logs and itsm-results share a unique per-run suffix. 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 9bc0fa41..2fc5799e 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.78' + GENERATED_AGAINST_MODULE_VERSION: '0.8.79' # 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 569f0ce9..d78255d5 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.78' + value: '0.8.79' # 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 e6ade4f7..55732a42 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.78' + GENERATED_AGAINST_MODULE_VERSION: '0.8.79' # 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 96b84a77..a3cb197a 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.78' + GENERATED_AGAINST_MODULE_VERSION: '0.8.79' # 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 a0db44af..12ef9983 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.78' + GENERATED_AGAINST_MODULE_VERSION: '0.8.79' # 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 55156393..676a8c50 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.78' + value: '0.8.79' # 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 80e87e01..33a9c9a2 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.78' + value: '0.8.79' # 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 9b1ebdfc..0b9da870 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.78' + GENERATED_AGAINST_MODULE_VERSION: '0.8.79' 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 274654ee..9fffb6d6 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.78' + GENERATED_AGAINST_MODULE_VERSION: '0.8.79' # Resolution order for the module version pin (leave all unset to install the latest, # which is the default "fix-forward" behaviour): queue-time parameter > pipeline variable # 'REQUIRED_MODULE_VERSION' overridden at queue time > empty (latest). diff --git a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/docs/appendix-pipelines.md b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/docs/appendix-pipelines.md index 0629fd06..84a24b78 100644 --- a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/docs/appendix-pipelines.md +++ b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/docs/appendix-pipelines.md @@ -162,6 +162,8 @@ The table below is the ground truth for what each shipped YAML does **out of the > **Behaviour change in v0.8.78**: per-cluster `ScheduleBlocked` / `SideloadedBlocked` / `ExcludedByTag` outcomes now render as JUnit `` instead of `` so `dorny/test-reporter` no longer flips the Step.07 check RED on by-design gate-respect outcomes - `HealthCheckBlocked` deliberately stays a `` because a critical health failure IS actionable. The Step 7 Readiness KPI table also gains optional **Already Up to Date** and **Not Ready (needs attention before updating)** rows, wired in both shipped `apply-updates.yml` templates from the upstream `readiness.UpToDateCount` / `readiness.NotReadyCount` outputs (emitted since v0.8.74), so operators see the full ring breakdown - eligible / started / skipped / blocked / up-to-date / needs-attention - in one place. GitHub Actions only: `actions/download-artifact@v6 -> @v7` to silence the Node 20 deprecation warning (v7 chosen over v8 because v8 has a breaking `digest-mismatch=error` default). +> **New in v0.8.79 - operator break-glass override (`Force Immediate Update`)**: optional pipeline parameter on `apply-updates.yml` (`force_immediate_update` GitHub Actions choice input / `forceImmediateUpdate` Azure DevOps boolean parameter) defaulting to `false`. When set to `true` on a **manual run / queue only**, bypasses the per-cluster `UpdateStartWindow` / `UpdateExclusionsWindow` Step 3c maintenance-window gate for emergency / out-of-window patching. Defence in depth: GitHub Actions YAML collapses the flag to `'false'` for any non-`workflow_dispatch` event (`${{ github.event_name == 'workflow_dispatch' && github.event.inputs.force_immediate_update || 'false' }}`); Azure DevOps rechecks `$(Build.Reason) -eq 'Manual'` at runtime before honouring the parameter. A `WARNING:` GUI label is prepended on both pipeline hosts; a high-visibility `::warning::` (GHA) / `##vso[task.logissue type=warning]` (ADO) banner is emitted into the run log when the override fires. The override is unreachable from the `apply-updates-schedule.yml` configuration file (no `forceImmediateUpdate` field exists on `New-AzLocalApplyUpdatesScheduleConfig` / `Resolve-AzLocalPipelineUpdateRing` / `Get-AzLocalApplyUpdatesScheduleAudit`). Other readiness gates (connectivity, health, sideload status, `UpdateExcluded` operator hard-override) continue to apply. Maps to `Invoke-AzLocalReadinessGatedClusterUpdate -ForceImmediateUpdate` -> `Start-AzLocalClusterUpdate -IgnoreScheduleTags` for direct cmdlet use. + | Aspect | Value | |---|---| | **Purpose** | Apply updates to clusters filtered by `UpdateRing` tag value. The actual mutation step; every other pipeline in this folder is either preparation, monitoring, or reporting. | 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 9a1ab300..b717697c 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.78' + GENERATED_AGAINST_MODULE_VERSION: '0.8.79' 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 64dce1b7..25fe94b9 100644 --- a/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/apply-updates.yml +++ b/AzLocal.UpdateManagement/Automation-Pipeline-Examples/github-actions/apply-updates.yml @@ -91,6 +91,19 @@ on: options: - 'false' - 'true' + # v0.8.79 break-glass override. ONLY honoured for workflow_dispatch runs + # (manual operator action); schedule-triggered runs deliberately do NOT + # have this input bound, so it cannot leak into the YAML configuration + # file. When true, Step 3c maintenance-window gating is bypassed and + # updates start regardless of UpdateStartWindow / UpdateExclusionsWindow. + force_immediate_update: + description: 'WARNING: FORCE IMMEDIATE UPDATE - bypass the per-cluster UpdateStartWindow / UpdateExclusionsWindow tags and apply updates immediately for the selected ring. Break-glass override for emergency / out-of-window patching. MANUAL RUNS ONLY - schedule-triggered runs ignore this flag.' + required: false + default: 'false' + type: choice + options: + - 'false' + - 'true' # --- ITSM Connector (ServiceNow) --- # Set raise_itsm_ticket=true to open ServiceNow incidents for clusters whose # Status matches an entry in the trigger matrix (default config: @@ -135,7 +148,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.78' + GENERATED_AGAINST_MODULE_VERSION: '0.8.79' # 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). @@ -378,6 +391,11 @@ jobs: INPUT_UPDATE_RING: ${{ needs.check-readiness.outputs.resolved_update_ring }} INPUT_UPDATE_NAME: ${{ github.event.inputs.update_name }} INPUT_DRY_RUN: ${{ github.event.inputs.dry_run }} + # v0.8.79 break-glass override. Force-immediate is ONLY honoured on + # workflow_dispatch runs. The conditional below collapses to 'false' + # for any non-manual event (schedule, push, etc.) so the override + # cannot leak through the YAML configuration into automated firings. + INPUT_FORCE_IMMEDIATE_UPDATE: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.force_immediate_update || 'false' }} # v0.7.90: ';'-joined allow-list resolved from apply-updates-schedule.yml # 'allowedUpdateVersions' (schema v2). Empty for workflow_dispatch or # when no allow-list is configured - in either case the cmdlet falls @@ -393,6 +411,7 @@ jobs: OutputDirectory = './artifacts' } if ($env:INPUT_DRY_RUN -eq 'true') { $params['DryRun'] = $true } + if ($env:INPUT_FORCE_IMMEDIATE_UPDATE -eq 'true') { $params['ForceImmediateUpdate'] = $true } Invoke-AzLocalReadinessGatedClusterUpdate @params - name: Compute Artifact Timestamp 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 436c2e68..b3169ca5 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.78' + GENERATED_AGAINST_MODULE_VERSION: '0.8.79' # 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 731926b4..f828f6e1 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.78' + GENERATED_AGAINST_MODULE_VERSION: '0.8.79' # 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 9cec88ec..820c0615 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.78' + GENERATED_AGAINST_MODULE_VERSION: '0.8.79' # 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 ea8e14a1..ac5337a0 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.78' + GENERATED_AGAINST_MODULE_VERSION: '0.8.79' # 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 15a9364a..a5454471 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.78' + GENERATED_AGAINST_MODULE_VERSION: '0.8.79' # 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 ed97e7ab..1d2c26bf 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.78' + GENERATED_AGAINST_MODULE_VERSION: '0.8.79' # 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 ca71e298..1cd12d43 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.78' + GENERATED_AGAINST_MODULE_VERSION: '0.8.79' # 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 610a0fcf..a93e48cc 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.78' + GENERATED_AGAINST_MODULE_VERSION: '0.8.79' # 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 b280ba75..221f6d1e 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.78' + GENERATED_AGAINST_MODULE_VERSION: '0.8.79' # 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 6a8e872a..c255d39e 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.78' + ModuleVersion = '0.8.79' # Supported PSEditions CompatiblePSEditions = @('Desktop', 'Core') @@ -309,21 +309,17 @@ # ReleaseNotes of this module ReleaseNotes = @' -## Version 0.8.78 - Patch: pipeline-summary UX polish driven by operator feedback. Three improvements land together: (1) JUnit re-classification - `ScheduleBlocked` / `SideloadedBlocked` / `ExcludedByTag` are designed operator-configured gate-respect outcomes, not failures; they no longer flip Step.07 runs RED via `dorny/test-reporter` and now render as `` (matches the summary `` attr). `HealthCheckBlocked` stays a failure (a Critical health failure blocking an update IS actionable). (2) Apply Updates summary now surfaces the FULL ring picture - the new `Add-AzLocalApplyUpdatesStepSummary -UpToDateCount` / `-NotReadyCount` parameters add `Already Up to Date` and `Not Ready (needs attention before updating)` rows to the Readiness KPI table, so the operator always sees how many clusters are healthy steady state (already patched) vs held back by the readiness gate (state=NeedsAttention / UpdateInProgress / UpdateFailed, pending SBE prereqs, critical health failures). Both apply-updates YAMLs (GitHub Actions + Azure DevOps) wire the upstream `readiness.UpToDateCount` / `readiness.NotReadyCount` outputs through. (3) `actions/download-artifact@v6` -> `@v7` bump in `apply-updates.yml` (GHA) - silences the Node.js 20 deprecation warning that v6 was emitting on every Step.07 run; v7 runs natively on Node 24. No public API change or new exports (still 60). All bundled pipeline templates bump `GENERATED_AGAINST_MODULE_VERSION` from `'0.8.77'` to `'0.8.78'`. +## Version 0.8.79 - Patch: operator break-glass override for Step.07. Adds `Force Immediate Update` to `Invoke-AzLocalReadinessGatedClusterUpdate` (`-ForceImmediateUpdate`) and `Start-AzLocalClusterUpdate` (`-IgnoreScheduleTags`), plus matching pipeline parameters `force_immediate_update` (GitHub Actions `workflow_dispatch` input) / `forceImmediateUpdate` (Azure DevOps `parameters:` boolean) both defaulting to `false`. When enabled, the per-cluster Step 3c maintenance-window gate (`UpdateStartWindow` / `UpdateExclusionsWindow` tags) is bypassed and updates start regardless of the current UTC time. Intended for emergency / out-of-window patching driven by an on-call operator. The override is UNREACHABLE from the apply-updates-schedule.yml configuration file - GHA wiring collapses the flag to `'false'` for any non-`workflow_dispatch` trigger (`${{ github.event_name == 'workflow_dispatch' && github.event.inputs.force_immediate_update || 'false' }}`), and ADO rechecks `$(Build.Reason) -eq 'Manual'` at runtime before honouring the parameter. A `WARNING:` GUI label is prepended on both pipeline hosts and a high-visibility `::warning::` (GHA) / `##vso[task.logissue type=warning]` (ADO) banner is emitted into the run log when the override fires. No new exports (still 60). All bundled pipeline templates bump `GENERATED_AGAINST_MODULE_VERSION` from `'0.8.78'` to `'0.8.79'`. -- **FIX (`Export-ResultsToJUnitXml`)**: `ScheduleBlocked` / `SideloadedBlocked` / `ExcludedByTag` previously emitted `` etc., which made `dorny/test-reporter` flip Step.07 runs RED with `##[error]Failed test were found` on otherwise successful schedule-aware / sideload-gated / tag-excluded runs. They now render as `` and the summary `` / `` attributes agree. `HealthCheckBlocked` deliberately remains in the failure bucket - a Critical health failure blocking an update is a real operational issue the team should action. -- **CHANGE (`Add-AzLocalApplyUpdatesStepSummary`)**: new optional `-UpToDateCount` / `-NotReadyCount` parameters (default `-1` = unknown, row omitted) expand the Readiness KPI table from 2 rows (Total / Ready) to 4 rows (Total / Ready / Already Up to Date / Not Ready). Up-to-Date is a healthy steady state (already fully patched); Not-Ready surfaces the count of clusters correctly held back by the readiness gate so the operator sees them in the apply-updates summary, not only in the no-ready-clusters summary. Backwards-compatible: existing callers that omit the new params see the same 2-row table they always did. -- **CHANGE (`apply-updates.yml` GitHub Actions + Azure DevOps)**: both templates now pipe the upstream check-readiness job/stage's `up_to_date_count` / `not_ready_count` outputs (already emitted since v0.8.74) through to the Generate Summary step. ADO additionally wires two new `variables:` (`upToDateCount` / `notReadyCount`) on the Apply Updates job pulling from `stageDependencies.CheckReadiness.ReadinessCheck.outputs['readiness.UpToDateCount' / '.NotReadyCount']`. -- **CHANGE (`apply-updates.yml` GitHub Actions)**: `actions/download-artifact@v6` -> `@v7` for the readiness-report download step. v6 still ran on Node.js 20 and was emitting `Node.js 20 actions are deprecated` warnings on every Step.07 run; v7 runs natively on Node 24 and silences the warning. v7 (not v8) is the safer choice because v8's breaking change defaults the `digest-mismatch` setting to `error`. -- **TESTS**: existing `Context 'JUnit XML export handles ScheduleBlocked'` and `'JUnit XML export handles SideloadedBlocked'` updated to assert `` instead of `` plus matching summary attrs (`[int]$suite.failures | Should -Be 0`, `[int]$suite.skipped | Should -Be 1`). New `It 'Export-ResultsToJUnitXml renders ExcludedByTag as (v0.8.78)'` regression. New `It 'Has v0.8.78 optional readiness-breakdown parameters UpToDateCount/NotReadyCount'` parameter-shape regression on `Add-AzLocalApplyUpdatesStepSummary`. -- **All bundled pipeline templates** bump `GENERATED_AGAINST_MODULE_VERSION` from `'0.8.77'` to `'0.8.78'`. +- **CHANGE (`Start-AzLocalClusterUpdate`)**: new `[switch]$IgnoreScheduleTags` parameter. When set, the entire Step 3c block is skipped (tag lookup + `Test-AzLocalUpdateScheduleAllowed` call) and a per-cluster `Warning` is logged with the bypassed tag values so the override is visible in the transcript. Does NOT bypass any other readiness gate (connectivity, health, sideload, `UpdateExcluded` operator hard-override). +- **CHANGE (`Invoke-AzLocalReadinessGatedClusterUpdate`)**: new `[switch]$ForceImmediateUpdate` parameter. When set, forwards `-IgnoreScheduleTags` to `Start-AzLocalClusterUpdate` for every cluster in the readiness CSV and emits a host-aware warning banner at the top of the apply. +- **CHANGE (`apply-updates.yml` GitHub Actions + Azure DevOps)**: new `force_immediate_update` choice input / `forceImmediateUpdate` boolean parameter with a `WARNING:` GUI label that calls out the override risk and the `MANUAL RUNS ONLY` / `MANUAL QUEUE ONLY` constraint. Both templates enforce the manual-only constraint at the YAML layer (GHA `&&/||` collapse + ADO `Build.Reason` runtime check) so a scheduled cron firing can never honour the flag. +- **TESTS**: parameter-presence regressions for both new switches; new `v0.8.79 Step.7 break-glass` YAML smoke-test context with 8 assertions covering the GHA / ADO input declarations, anti-injection guards, and a sweep of `Resolve-AzLocalPipelineUpdateRing` / `New-AzLocalApplyUpdatesScheduleConfig` / `Get-AzLocalApplyUpdatesScheduleAudit` confirming the override is NOT referenced from any schedule-config code path. +- **All bundled pipeline templates** bump `GENERATED_AGAINST_MODULE_VERSION` from `'0.8.78'` to `'0.8.79'`. -## 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'`. +## Version 0.8.78 - Patch: pipeline-summary UX polish. (1) JUnit re-classification - `ScheduleBlocked` / `SideloadedBlocked` / `ExcludedByTag` are designed gate-respect outcomes, now render as `` so `dorny/test-reporter` no longer flips Step.07 RED (`HealthCheckBlocked` stays ``). (2) `Add-AzLocalApplyUpdatesStepSummary` gains optional `-UpToDateCount` / `-NotReadyCount` for full ring breakdown in the Readiness KPI table (both `apply-updates.yml` templates wire the upstream `readiness.UpToDateCount` / `readiness.NotReadyCount` outputs through). (3) `actions/download-artifact@v6` -> `@v7` in `apply-updates.yml` (GHA) silences the Node.js 20 deprecation warning. No public API or export-count change (still 60). All bundled pipeline templates bump `GENERATED_AGAINST_MODULE_VERSION` from `'0.8.77'` to `'0.8.78'`. See CHANGELOG.md / docs/release-history.md for the full v0.8.78 entry. -- **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.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`) in `Start-AzLocalClusterUpdate` (UpdateStartWindow / UpdateExclusionsWindow tag lookup) and in `Test-AzLocalClusterHealth` / `Get-HealthCheckFailureSummary` / `Get-AzLocalFleetStatusData` (`properties.healthCheckResult` access). No public API or export-count change (still 60). All bundled pipeline templates bump `GENERATED_AGAINST_MODULE_VERSION` from `'0.8.76'` to `'0.8.77'`. See CHANGELOG.md / docs/release-history.md for the full v0.8.77 entry. ## Version 0.8.76 - Patch: adds a Microsoft-hosted Windows preflight job (GitHub Actions) / preflight stage (Azure DevOps) in front of the opt-in Step.6 `sideload-updates.yml` pipeline so triggering it without first completing the opt-in setup produces an actionable panel rather than a silent `Status: Skipped`. Also broadens the master gate to accept `'true'` / `'True'` / `'TRUE'` / `'1'`. See CHANGELOG for the full v0.8.76 entry. diff --git a/AzLocal.UpdateManagement/AzLocal.UpdateManagement.psm1 b/AzLocal.UpdateManagement/AzLocal.UpdateManagement.psm1 index fd3ed94d..e6af44a7 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.78' +$script:ModuleVersion = '0.8.79' $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 1d735e73..c2b035f1 100644 --- a/AzLocal.UpdateManagement/CHANGELOG.md +++ b/AzLocal.UpdateManagement/CHANGELOG.md @@ -5,6 +5,35 @@ 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.79] - 2026-06-15 + +Patch release. Adds an operator-only **Force Immediate Update** break-glass override to the Step.07 apply-updates pipeline that bypasses the per-cluster `UpdateStartWindow` / `UpdateExclusionsWindow` maintenance-window gate. Intended for emergency / out-of-window patching driven by an on-call operator; defaults to OFF; cannot be reached from the scheduled `apply-updates-schedule.yml` configuration file. + +### Added + +- **`Start-AzLocalClusterUpdate -IgnoreScheduleTags`** (new `[switch]` parameter). When set, the entire Step 3c maintenance-window block is skipped: the per-cluster `UpdateStartWindow` and `UpdateExclusionsWindow` tag lookups still execute (so the tag values can be logged), but `Test-AzLocalUpdateScheduleAllowed` is NOT called and the cluster is admitted regardless of the current UTC time. A `Warning`-level log entry is emitted per cluster recording the bypassed tag values so the override is visible in the transcript. Other gates (connectivity, health, sideload status, `UpdateExcluded` operator hard-override) continue to apply. +- **`Invoke-AzLocalReadinessGatedClusterUpdate -ForceImmediateUpdate`** (new `[switch]` parameter). When set, forwards `-IgnoreScheduleTags` to `Start-AzLocalClusterUpdate` for every cluster in the readiness CSV, and emits a high-visibility warning banner at the top of the apply run using the host-aware idiom (`::warning::` for GitHub Actions, `##vso[task.logissue type=warning]` for Azure DevOps, plain `Write-Warning` otherwise) so operators see the override has fired before any cluster is touched. +- **Pipeline parameter `force_immediate_update` (GitHub Actions `workflow_dispatch` input)** in `apply-updates.yml`. Choice input with values `false` (default) / `true`. The `description:` rendered in the GUI starts `WARNING:` and explicitly notes `MANUAL RUNS ONLY`. Wired into the apply step via an `INPUT_FORCE_IMMEDIATE_UPDATE` environment variable using an anti-injection collapse pattern: `${{ github.event_name == 'workflow_dispatch' && github.event.inputs.force_immediate_update || 'false' }}` - any `schedule` / `push` / `pull_request` / `repository_dispatch` event sees `'false'` even if a malicious upstream tried to inject the input. +- **Pipeline parameter `forceImmediateUpdate` (Azure DevOps `parameters:` boolean)** in `apply-updates.yml`. Defaults to `false`. The `displayName:` rendered in the Queue Build GUI starts `WARNING:` and explicitly notes `MANUAL QUEUE ONLY`. Wired into the apply step via a `FORCE_IMMEDIATE_UPDATE_PARAM` environment variable, with a runtime guard: the override is only forwarded to `Invoke-AzLocalReadinessGatedClusterUpdate` when `$env:BUILD_REASON -eq 'Manual'`. Scheduled / CI-triggered / PR-triggered runs log a `##vso[task.logissue type=warning]` explaining the parameter was ignored. +- **Bundled pipeline templates** all bump `GENERATED_AGAINST_MODULE_VERSION` from `'0.8.78'` to `'0.8.79'`. + +### Security / Anti-Injection Design + +- The override is **deliberately unreachable from the apply-updates-schedule.yml configuration file**. No `forceImmediateUpdate` field exists on `New-AzLocalApplyUpdatesScheduleConfig`, `Resolve-AzLocalPipelineUpdateRing`, or `Get-AzLocalApplyUpdatesScheduleAudit`. A new Pester sweep asserts this. +- **Two layers of defence prevent a scheduled cron firing from honouring the flag**: (1) GitHub Actions YAML expression collapse to `'false'` for any non-`workflow_dispatch` event; (2) Azure DevOps runtime PowerShell guard requiring `$(Build.Reason) -eq 'Manual'`. Both layers fail closed. +- The `WARNING:` GUI label is required to make the override visible to anyone clicking through the Queue Build / Run Workflow dialog. + +### Tests + +- New parameter-presence regressions for `Start-AzLocalClusterUpdate -IgnoreScheduleTags` and `Invoke-AzLocalReadinessGatedClusterUpdate -ForceImmediateUpdate`. +- New `v0.8.79 Step.7 break-glass: force_immediate_update / forceImmediateUpdate` context (8 `It` blocks) that asserts: GHA input declaration + `WARNING:` GUI label + `MANUAL RUNS ONLY` text; ADO parameter declaration + `WARNING:` displayName + `MANUAL QUEUE ONLY` text; GHA `${{ github.event_name == 'workflow_dispatch' && ... || 'false' }}` anti-injection collapse; ADO `$(Build.Reason) -eq 'Manual'` runtime guard; pwsh forwarding of `-ForceImmediateUpdate` on both hosts; anti-leak sweep across `New-AzLocalApplyUpdatesScheduleConfig.ps1`, `Resolve-AzLocalPipelineUpdateRing.ps1`, and `Get-AzLocalApplyUpdatesScheduleAudit.ps1` confirming the override is NEVER referenced from any schedule-config code path. +- `Should have version 0.8.79` updated. + +### Notes + +- This release is operator-UX-only: no Az cmdlet API change, no readiness gate change, no schedule-config change, no new exports (still 60). +- The `UpdateExcluded` operator hard-override (`AzureLocalManagement.UpdateExcluded=true`) is INTENTIONALLY still respected by `Force Immediate Update`. The break-glass override is about time-window gates, not about per-cluster opt-outs. If a cluster must be force-included despite an `UpdateExcluded` tag, the operator removes the tag first. + ## [0.8.78] - 2026-06-15 Patch release. Pipeline-summary UX polish driven by operator feedback on the bundled apply-updates pipeline (Step.07). Three improvements land together, plus the routine `actions/download-artifact` deprecation bump: diff --git a/AzLocal.UpdateManagement/Public/Invoke-AzLocalReadinessGatedClusterUpdate.ps1 b/AzLocal.UpdateManagement/Public/Invoke-AzLocalReadinessGatedClusterUpdate.ps1 index bfd668f0..46181c13 100644 --- a/AzLocal.UpdateManagement/Public/Invoke-AzLocalReadinessGatedClusterUpdate.ps1 +++ b/AzLocal.UpdateManagement/Public/Invoke-AzLocalReadinessGatedClusterUpdate.ps1 @@ -54,6 +54,15 @@ function Invoke-AzLocalReadinessGatedClusterUpdate { Returns PSCustomObject with all seven counters + Results + JUnitPath + ApplyResultsJsonPath + ReadyResourceIds (the ARM IDs actually handed to Start-AzLocalClusterUpdate). + .PARAMETER ForceImmediateUpdate + v0.8.79 break-glass override. When set, forwards `-IgnoreScheduleTags` to + Start-AzLocalClusterUpdate so the per-cluster Step 3c maintenance-schedule + gate (UpdateStartWindow / UpdateExclusionsWindow tags) is BYPASSED for every + cluster in the readiness CSV. Intended for emergency / out-of-window patching + driven by an on-call operator. The bundled apply-updates.yml pipelines only + expose this through `workflow_dispatch` (GHA) and `Manual` queue (ADO); schedule- + triggered runs never honour it (guarded at the YAML layer). A prominent banner + is logged at the start of the apply. .NOTES Author : AzLocal.UpdateManagement Version : 0.8.5 (Step.6 thin-YAML port) @@ -93,7 +102,9 @@ function Invoke-AzLocalReadinessGatedClusterUpdate { [ValidateNotNullOrEmpty()] [string]$ApplyResultsJsonFileName = 'apply-results.json', - [switch]$PassThru + [switch]$PassThru, + + [switch]$ForceImmediateUpdate ) Set-StrictMode -Version Latest @@ -225,6 +236,20 @@ function Invoke-AzLocalReadinessGatedClusterUpdate { } } + if ($ForceImmediateUpdate) { + # v0.8.79 break-glass override. Surface a HIGH-VISIBILITY warning on every + # supported pipeline host so the override is unmistakable in run logs and + # PR annotations. The actual bypass is enforced inside Start-AzLocalClusterUpdate + # Step 3c via the forwarded -IgnoreScheduleTags switch. + $applyParams['IgnoreScheduleTags'] = $true + $forceMsg = "FORCE IMMEDIATE UPDATE enabled. UpdateStartWindow and UpdateExclusionsWindow tags will be IGNORED for every cluster in this run. This is a break-glass override intended for emergency / out-of-window patching." + switch ($pipelineHost) { + 'GitHub' { Write-Host "::warning::$forceMsg" } + 'AzureDevOps' { Write-Host "##vso[task.logissue type=warning]$forceMsg" } + default { Write-Warning $forceMsg } + } + } + Write-Host "" Write-Host "========================================" -ForegroundColor Cyan Write-Host "Applying Updates to UpdateRing: $UpdateRing" -ForegroundColor Cyan diff --git a/AzLocal.UpdateManagement/Public/Start-AzLocalClusterUpdate.ps1 b/AzLocal.UpdateManagement/Public/Start-AzLocalClusterUpdate.ps1 index a7ae93bc..7e3df584 100644 --- a/AzLocal.UpdateManagement/Public/Start-AzLocalClusterUpdate.ps1 +++ b/AzLocal.UpdateManagement/Public/Start-AzLocalClusterUpdate.ps1 @@ -48,6 +48,17 @@ function Start-AzLocalClusterUpdate { Azure REST API version to use. Default: "2025-10-01". .PARAMETER Force Skip confirmation prompts. + .PARAMETER IgnoreScheduleTags + v0.8.79 break-glass override. When set, the per-cluster Step 3c maintenance-schedule + gate is BYPASSED entirely - both the `UpdateStartWindow` (allowed window) and the + `UpdateExclusionsWindow` (deny window) tags are ignored and the cluster proceeds + directly to the apply call regardless of the current UTC time. A `Warning` is logged + per cluster so the override is visible in the run log + transcript. This switch does + NOT bypass any of the other readiness gates (cluster connectivity, health, sideload, + UpdateExcluded operator hard-override). Intended for emergency / out-of-window + patching driven by an on-call operator from a manual pipeline run; the bundled + pipeline YAMLs only expose this through `workflow_dispatch` (GHA) / `Manual` queue + (ADO) - schedule-triggered runs never honour it. .PARAMETER LogFolderPath Folder path for log files. Default: C:\ProgramData\AzLocal.UpdateManagement\ .PARAMETER EnableTranscript @@ -108,6 +119,9 @@ function Start-AzLocalClusterUpdate { [Parameter(Mandatory = $false)] [switch]$Force, + [Parameter(Mandatory = $false)] + [switch]$IgnoreScheduleTags, + [Parameter(Mandatory = $false)] [string]$LogFolderPath, @@ -814,7 +828,13 @@ function Start-AzLocalClusterUpdate { } } - if ($windowTagValue -or $exclusionTagValue) { + if ($IgnoreScheduleTags) { + # v0.8.79 break-glass override (-IgnoreScheduleTags / pipeline force_immediate_update=true). + # Skip Step 3c entirely - apply regardless of UpdateStartWindow / UpdateExclusionsWindow. + # Log a Warning per cluster so the override is visible in the transcript + step summary. + Write-Log -Message "Step 3c: BYPASSED for '$clusterName' (-IgnoreScheduleTags). UpdateStartWindow='$windowTagValue', UpdateExclusionsWindow='$exclusionTagValue' will NOT be evaluated." -Level Warning + } + elseif ($windowTagValue -or $exclusionTagValue) { Write-Log -Message "Step 3c: Checking maintenance schedule tags..." -Level Info if ($windowTagValue) { Write-Log -Message " UpdateStartWindow tag: $windowTagValue" -Level Info } if ($exclusionTagValue) { Write-Log -Message " UpdateExclusionsWindow tag: $exclusionTagValue" -Level Info } diff --git a/AzLocal.UpdateManagement/README.md b/AzLocal.UpdateManagement/README.md index 5c880ffb..e431d053 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.78 - [Published in PowerShell Gallery](https://www.powershellgallery.com/packages/AzLocal.UpdateManagement/0.8.78) +**Latest Version:** v0.8.79 - [Published in PowerShell Gallery](https://www.powershellgallery.com/packages/AzLocal.UpdateManagement/0.8.79) > 📢 **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.78](#whats-new-in-v0878) +- [What's New in v0.8.79](#whats-new-in-v0879) - [Files](#files) - [Prerequisites](#prerequisites) - [RBAC Requirements](#rbac-requirements) (summary; full reference in [docs/rbac.md](docs/rbac.md)) @@ -86,19 +86,32 @@ 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.78 +## What's New in v0.8.79 -**Patch release. Pipeline-summary UX polish** driven by operator feedback on the bundled apply-updates pipeline (Step.07). Three improvements land together: +**Patch release. Adds an operator-only "Force Immediate Update" break-glass override** to the Step.07 apply-updates pipeline. Intended for emergency / out-of-window patching driven by an on-call operator; **defaults to OFF**; **cannot be reached from the scheduled `apply-updates-schedule.yml` configuration file**. -1. **JUnit re-classification of designed gate-respect outcomes.** `ScheduleBlocked` (cluster honouring its `UpdateStartWindow`), `SideloadedBlocked` (cluster honouring `UpdateSideloaded=False`), and `ExcludedByTag` (cluster honouring `AzureLocalManagement.UpdateExcluded=true`) are operator-configured opt-outs, not failures. They were previously rendered as `` rows by `Export-ResultsToJUnitXml`, which made `dorny/test-reporter` flip Step.07 runs **red** with `##[error]Failed test were found` on otherwise-successful schedule-aware / sideload-gated / tag-excluded runs. They now render as `` and the summary `` / `` attributes agree. `HealthCheckBlocked` deliberately remains in the failure bucket - a Critical health failure blocking an update IS a real operational issue the team should action. -2. **Apply Updates summary now shows the FULL ring picture.** Previously the Readiness KPI table in the Apply Updates step summary showed only Total Clusters + Ready for Update, meaning the operator had no idea how many clusters were already fully patched (healthy steady state) or held back by the readiness gate (`state=NeedsAttention` / `UpdateInProgress` / `UpdateFailed` / pending SBE prerequisites / Critical health failure). The new optional `-UpToDateCount` / `-NotReadyCount` parameters on `Add-AzLocalApplyUpdatesStepSummary` add `Already Up to Date` and `Not Ready (needs attention before updating)` rows. Both apply-updates YAMLs (GitHub Actions + Azure DevOps) wire the upstream `readiness.UpToDateCount` / `readiness.NotReadyCount` outputs (already emitted by the check-readiness job/stage since v0.8.74) through to the summary task. -3. **`actions/download-artifact@v6` -> `@v7` in `apply-updates.yml` (GitHub Actions).** v6 still ran on Node.js 20 and was emitting `Node.js 20 actions are deprecated. Please update the following actions to use Node.js 24` warnings on every Step.07 run. `@v7` (Dec 2025 release) runs natively on Node 24 and silences the warning. `@v7` is chosen deliberately over `@v8`: v8 introduced a breaking change defaulting the `digest-mismatch` setting to `error`, which is more disruptive than the v6 -> v7 upgrade benefit. +### What it does -Readiness gate verified robust against the operator question "do we show some clusters as Ready when they have a previously-failed in-progress run?" - the answer is **no**. `Get-AzLocalClusterUpdateReadiness` and `Start-AzLocalClusterUpdate` (Step 3) both predicate Ready on `updateSummary.properties.state -in @('UpdateAvailable','Ready','ReadyToInstall')`. Clusters in `NeedsAttention` / `UpdateInProgress` / `UpdateFailed` / `PreparationFailed` correctly land in Not-Ready and are surfaced by the Cluster Readiness table's `Status` column (added in v0.8.74). +When enabled, the per-cluster Step 3c maintenance-window gate (`UpdateStartWindow` / `UpdateExclusionsWindow` tags) is **bypassed** and updates start regardless of the current UTC time. All other readiness gates (connectivity, health, sideload status, `UpdateExcluded` operator hard-override) continue to apply. The override is opt-in per-run on both pipeline hosts: -`GENERATED_AGAINST_MODULE_VERSION` bumped from `0.8.77` to `0.8.78` across all bundled pipeline templates. +- **GitHub Actions:** new `force_immediate_update` `workflow_dispatch` choice input (default `'false'`). Description rendered in the Run Workflow GUI starts with `WARNING:` and explicitly notes `MANUAL RUNS ONLY`. +- **Azure DevOps:** new `forceImmediateUpdate` boolean `parameters:` entry (default `false`). `displayName:` in the Queue Build GUI starts with `WARNING:` and explicitly notes `MANUAL QUEUE ONLY`. +- **Direct cmdlet use:** new `Invoke-AzLocalReadinessGatedClusterUpdate -ForceImmediateUpdate` switch (which forwards `-IgnoreScheduleTags` to `Start-AzLocalClusterUpdate` for every cluster in the readiness CSV). A high-visibility warning banner (`::warning::` on GHA, `##vso[task.logissue type=warning]` on ADO, `Write-Warning` otherwise) is emitted at the top of the run when the override fires. -See [CHANGELOG.md](CHANGELOG.md#0878---2026-06-15) for the full v0.8.78 entry. See [`What's New in v0.8.77`](docs/release-history.md#whats-new-in-v0877) in the Release History for the previous release. +### Security / anti-injection design + +Two layers of defence prevent a scheduled cron firing from honouring the override: + +1. **GitHub Actions YAML expression collapse**: `${{ github.event_name == 'workflow_dispatch' && github.event.inputs.force_immediate_update || 'false' }}` - any `schedule` / `push` / `pull_request` / `repository_dispatch` event sees `'false'` even if a malicious upstream tried to inject the input. +2. **Azure DevOps runtime guard**: the apply step rechecks `$(Build.Reason) -eq 'Manual'` before flipping the switch. Scheduled / CI / PR runs log a `##vso[task.logissue type=warning]` explaining the parameter was ignored. + +The override is **deliberately not surfaced** by `New-AzLocalApplyUpdatesScheduleConfig` / `Resolve-AzLocalPipelineUpdateRing` / `Get-AzLocalApplyUpdatesScheduleAudit` - there is no path to enable it from `apply-updates-schedule.yml`. A Pester sweep asserts this. + +The `UpdateExcluded` operator hard-override (`AzureLocalManagement.UpdateExcluded=true`) is INTENTIONALLY still respected by `Force Immediate Update` - the break-glass override is about time-window gates, not per-cluster opt-outs. If a cluster must be force-included despite an `UpdateExcluded` tag, remove the tag first. + +`GENERATED_AGAINST_MODULE_VERSION` bumped from `0.8.78` to `0.8.79` across all bundled pipeline templates. + +See [CHANGELOG.md](CHANGELOG.md#0879---2026-06-15) for the full v0.8.79 entry. See [`What's New in v0.8.78`](docs/release-history.md#whats-new-in-v0878) in the Release History for the previous release. ## Files @@ -577,7 +590,11 @@ This code is provided as-is for educational and reference purposes. The full What's-New history (v0.7.81 and earlier) has moved to [docs/release-history.md](docs/release-history.md). -The most recent release notes for **v0.8.78** stay above under [`What's New in v0.8.78`](#whats-new-in-v0878). +The most recent release notes for **v0.8.79** stay above under [`What's New in v0.8.79`](#whats-new-in-v0879). + +### What's New in v0.8.78 + +**Patch release. Pipeline-summary UX polish** on the bundled apply-updates pipeline (Step.07). Three improvements landed together: (1) `ScheduleBlocked` / `SideloadedBlocked` / `ExcludedByTag` outcomes now render as JUnit `` instead of `` so `dorny/test-reporter` no longer flips Step.07 RED on by-design gate-respect outcomes (`HealthCheckBlocked` stays a ``); (2) the new optional `-UpToDateCount` / `-NotReadyCount` parameters on `Add-AzLocalApplyUpdatesStepSummary` add `Already Up to Date` and `Not Ready (needs attention before updating)` rows to the Readiness KPI table; (3) `actions/download-artifact@v6 -> @v7` in `apply-updates.yml` (GitHub Actions) silences the Node.js 20 deprecation warning. No public API change or new exports (still 60). `GENERATED_AGAINST_MODULE_VERSION` bumped from `0.8.77` to `0.8.78` across all bundled pipeline templates. See [CHANGELOG.md](CHANGELOG.md#0878---2026-06-15) for the full v0.8.78 entry and [docs/release-history.md](docs/release-history.md#whats-new-in-v0878) for the archived entry. ### What's New in v0.8.77 diff --git a/AzLocal.UpdateManagement/Tests/AzLocal.UpdateManagement.Tests.ps1 b/AzLocal.UpdateManagement/Tests/AzLocal.UpdateManagement.Tests.ps1 index c8d9b4ec..f90208e6 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.78' { - $script:ModuleInfo.Version | Should -Be '0.8.78' + It 'Should have version 0.8.79' { + $script:ModuleInfo.Version | Should -Be '0.8.79' } It 'Module version constants are in sync between .psm1 and .psd1' { @@ -806,6 +806,77 @@ Describe 'Module: AzLocal.UpdateManagement' { } } + Context 'v0.8.79 Step.7 break-glass: force_immediate_update / forceImmediateUpdate' { + # v0.8.79 introduces an operator-only override that bypasses the per-cluster + # UpdateStartWindow / UpdateExclusionsWindow gate (Step 3c of Start-AzLocalClusterUpdate). + # The override MUST NOT be reachable from the apply-updates-schedule.yml configuration + # file - this is enforced by per-host wiring guards below. + BeforeAll { + $script:S6Gh78 = (Resolve-Path (Join-Path -Path $PSScriptRoot -ChildPath '..\Automation-Pipeline-Examples\github-actions\apply-updates.yml')).Path + $script:S6Ado78 = (Resolve-Path (Join-Path -Path $PSScriptRoot -ChildPath '..\Automation-Pipeline-Examples\azure-devops\apply-updates.yml')).Path + $script:S6Gh78Content = Get-Content -LiteralPath $script:S6Gh78 -Raw + $script:S6Ado78Content = Get-Content -LiteralPath $script:S6Ado78 -Raw + } + + It 'GH Step.7 declares the force_immediate_update workflow_dispatch input (choice false/true, defaults false)' { + $script:S6Gh78Content | Should -Match '(?m)^\s*force_immediate_update:\s*$' -Because 'v0.8.79 GH apply-updates must expose force_immediate_update as a workflow_dispatch input' + $script:S6Gh78Content | Should -Match "force_immediate_update:[\s\S]{0,600}?type:\s*choice[\s\S]{0,200}?-\s*'false'[\s\S]{0,100}?-\s*'true'" -Because 'force_immediate_update must be a choice between false and true' + $script:S6Gh78Content | Should -Match "force_immediate_update:[\s\S]{0,600}?default:\s*'false'" -Because 'force_immediate_update must default to false (opt-in only)' + } + + It 'GH Step.7 input description starts with WARNING and flags MANUAL RUNS ONLY' { + $script:S6Gh78Content | Should -Match "force_immediate_update:[\s\S]{0,200}?description:\s*'WARNING:" -Because 'GUI label must visibly call out the override risk' + $script:S6Gh78Content | Should -Match 'force_immediate_update[\s\S]{0,600}?MANUAL RUNS ONLY' -Because 'description must state that schedule-triggered runs ignore the flag' + } + + It 'GH Step.7 apply step env: clamps force_immediate_update to false on non-workflow_dispatch events (anti-injection)' { + # The conditional MUST collapse to 'false' for any event other than workflow_dispatch + # so a scheduled cron firing can never honour the override even if a YAML config + # change ever tried to flip the default. + $script:S6Gh78Content | Should -Match "INPUT_FORCE_IMMEDIATE_UPDATE:\s*\$\{\{\s*github\.event_name\s*==\s*'workflow_dispatch'\s*&&\s*github\.event\.inputs\.force_immediate_update\s*\|\|\s*'false'\s*\}\}" -Because 'env wiring must collapse to false for any non-workflow_dispatch trigger' + } + + It 'GH Step.7 apply pwsh forwards INPUT_FORCE_IMMEDIATE_UPDATE to the cmdlet as -ForceImmediateUpdate' { + $script:S6Gh78Content | Should -Match "if\s*\(\s*\`$env:INPUT_FORCE_IMMEDIATE_UPDATE\s*-eq\s*'true'\s*\)\s*\{\s*\`$params\['ForceImmediateUpdate'\]\s*=\s*\`$true\s*\}" + } + + It 'ADO Step.7 declares the forceImmediateUpdate boolean parameter (defaults false)' { + $script:S6Ado78Content | Should -Match '(?m)^\s*-\s*name:\s*forceImmediateUpdate' -Because 'v0.8.79 ADO apply-updates must expose forceImmediateUpdate as a top-level parameter' + $script:S6Ado78Content | Should -Match "forceImmediateUpdate[\s\S]{0,600}?type:\s*boolean[\s\S]{0,200}?default:\s*false" -Because 'forceImmediateUpdate must be a boolean defaulting to false' + } + + It 'ADO Step.7 forceImmediateUpdate displayName starts with WARNING and flags MANUAL QUEUE ONLY' { + $script:S6Ado78Content | Should -Match "forceImmediateUpdate[\s\S]{0,300}?displayName:\s*'WARNING:" -Because 'GUI label must visibly call out the override risk' + $script:S6Ado78Content | Should -Match 'forceImmediateUpdate[\s\S]{0,600}?MANUAL QUEUE ONLY' -Because 'displayName must state that schedule-triggered runs ignore the flag' + } + + It 'ADO Step.7 apply task env: exposes FORCE_IMMEDIATE_UPDATE_PARAM and BUILD_REASON' { + $script:S6Ado78Content | Should -Match 'FORCE_IMMEDIATE_UPDATE_PARAM:\s*\$\{\{\s*parameters\.forceImmediateUpdate\s*\}\}' + $script:S6Ado78Content | Should -Match 'BUILD_REASON:\s*\$\(Build\.Reason\)' + } + + It 'ADO Step.7 apply task pwsh enforces Build.Reason -eq Manual before honouring the override (anti-injection)' { + # The runtime guard MUST verify Build.Reason='Manual' before flipping the switch; + # any other reason (Schedule, IndividualCI, PullRequest, etc.) must log a warning + # and skip the override regardless of the parameter value. + $script:S6Ado78Content | Should -Match "if\s*\(\s*\`$env:FORCE_IMMEDIATE_UPDATE_PARAM\s*-eq\s*'True'\s*\)\s*\{[\s\S]{0,400}?\`$env:BUILD_REASON\s*-eq\s*'Manual'[\s\S]{0,200}?\`$params\['ForceImmediateUpdate'\]\s*=\s*\`$true" + } + + It 'apply-updates-schedule.yml does NOT expose a force_immediate / forceImmediate field (override must be unreachable from the schedule config)' { + # The schedule resolver generator must NOT learn about this override. Sweep every + # known schedule-config artefact to make sure no future refactor leaks the flag. + $candidates = @( + (Join-Path -Path $PSScriptRoot -ChildPath '..\Public\New-AzLocalApplyUpdatesScheduleConfig.ps1'), + (Join-Path -Path $PSScriptRoot -ChildPath '..\Public\Resolve-AzLocalPipelineUpdateRing.ps1'), + (Join-Path -Path $PSScriptRoot -ChildPath '..\Public\Get-AzLocalApplyUpdatesScheduleAudit.ps1') + ) | Where-Object { Test-Path -LiteralPath $_ } + foreach ($f in $candidates) { + $raw = Get-Content -LiteralPath $f -Raw + $raw | Should -Not -Match '(?i)force[_-]?immediate' -Because "$([IO.Path]::GetFileName($f)) must NOT reference the break-glass override (it is operator-only)" + } + } + } + Context 'v0.8.4 RBAC custom role display name rename' { # v0.8.4: the bundled custom role definition is renamed from # 'Azure Stack HCI Update Operator' to @@ -935,6 +1006,11 @@ Describe 'Function: Start-AzLocalClusterUpdate' { $command.Parameters.Keys | Should -Contain 'Force' } + It 'v0.8.79: Should have IgnoreScheduleTags switch (break-glass override - bypasses Step 3c UpdateStartWindow / UpdateExclusionsWindow gate)' { + $command.Parameters.Keys | Should -Contain 'IgnoreScheduleTags' + $command.Parameters['IgnoreScheduleTags'].ParameterType.Name | Should -Be 'SwitchParameter' + } + It 'Should have WhatIf parameter' { $command.Parameters.Keys | Should -Contain 'WhatIf' } @@ -17338,6 +17414,10 @@ Describe 'Thin-YAML Step.6: Invoke-AzLocalReadinessGatedClusterUpdate' { $script:S6CmdI.Parameters.Keys | Should -Contain 'PassThru' $script:S6CmdI.Parameters['PassThru'].ParameterType.Name | Should -Be 'SwitchParameter' } + It 'v0.8.79: Has parameter ForceImmediateUpdate (switch - break-glass override forwarded as -IgnoreScheduleTags)' { + $script:S6CmdI.Parameters.Keys | Should -Contain 'ForceImmediateUpdate' + $script:S6CmdI.Parameters['ForceImmediateUpdate'].ParameterType.Name | Should -Be 'SwitchParameter' + } } Context 'Empty-ready short-circuit (zero counters emitted, no apply attempted)' { diff --git a/AzLocal.UpdateManagement/Tests/test-run-timings.csv b/AzLocal.UpdateManagement/Tests/test-run-timings.csv index 5bdb6df6..d3db1402 100644 --- a/AzLocal.UpdateManagement/Tests/test-run-timings.csv +++ b/AzLocal.UpdateManagement/Tests/test-run-timings.csv @@ -12,3 +12,5 @@ TimestampUtc,ModuleVersion,Total,Passed,Failed,Skipped,WallClockSeconds,PesterDu "2026-06-12T15:30:21Z","0.8.76","1184","1145","0","1","100.71","98.24","3.89","Invoke-Tests.ps1","5.1.26100.8655" "2026-06-14T11:50:46Z","0.8.78","1189","1149","1","1","68.28","66.17","5.34","Invoke-Tests.ps1","5.1.26100.8655" "2026-06-14T11:51:58Z","0.8.78","1189","1150","0","1","49.95","48.7","3.74","Invoke-Tests.ps1","5.1.26100.8655" +"2026-06-14T18:27:18Z","0.8.79","1200","1160","1","1","86.13","84.83","9.85","Invoke-Tests.ps1","5.1.26100.8655" +"2026-06-14T18:29:15Z","0.8.79","1200","1161","0","1","46.34","45.08","2.96","Invoke-Tests.ps1","5.1.26100.8655" diff --git a/AzLocal.UpdateManagement/docs/release-history.md b/AzLocal.UpdateManagement/docs/release-history.md index f60028ae..fc296401 100644 --- a/AzLocal.UpdateManagement/docs/release-history.md +++ b/AzLocal.UpdateManagement/docs/release-history.md @@ -4,7 +4,21 @@ > > **For older releases**, this is the canonical reference; the main README intentionally stays slim so the most recent block is easy to find. > -> **For v0.8.78 (the current release)**, see the main [README.md](../README.md#whats-new-in-v0878) `What's New in v0.8.78` section. +> **For v0.8.79 (the current release)**, see the main [README.md](../README.md#whats-new-in-v0879) `What's New in v0.8.79` section. + +--- + +### What's New in v0.8.78 + +v0.8.78 was a pipeline-summary UX polish release driven by operator feedback on the bundled apply-updates pipeline (Step.07). Three improvements landed together: + +1. **JUnit re-classification of designed gate-respect outcomes.** `ScheduleBlocked` (cluster honouring its `UpdateStartWindow`), `SideloadedBlocked` (cluster honouring `UpdateSideloaded=False`), and `ExcludedByTag` (cluster honouring `AzureLocalManagement.UpdateExcluded=true`) are operator-configured opt-outs, not failures. They were previously rendered as `` rows by `Export-ResultsToJUnitXml`, which made `dorny/test-reporter` flip Step.07 runs **red** with `##[error]Failed test were found` on otherwise-successful schedule-aware / sideload-gated / tag-excluded runs. They now render as `` and the summary `` / `` attributes agree. `HealthCheckBlocked` deliberately remains in the failure bucket - a Critical health failure blocking an update IS a real operational issue the team should action. +2. **Apply Updates summary now shows the FULL ring picture.** The new optional `-UpToDateCount` / `-NotReadyCount` parameters on `Add-AzLocalApplyUpdatesStepSummary` add `Already Up to Date` and `Not Ready (needs attention before updating)` rows to the Readiness KPI table. Both apply-updates YAMLs (GitHub Actions + Azure DevOps) wire the upstream `readiness.UpToDateCount` / `readiness.NotReadyCount` outputs through to the summary task. +3. **`actions/download-artifact@v6` -> `@v7` in `apply-updates.yml` (GitHub Actions).** Silences the Node.js 20 deprecation warning. `@v7` chosen deliberately over `@v8` to avoid v8's `digest-mismatch=error` breaking-change default. + +`GENERATED_AGAINST_MODULE_VERSION` bumped from `0.8.77` to `0.8.78` across all bundled pipeline templates. + +See [CHANGELOG.md](../CHANGELOG.md#0878---2026-06-15) for the full v0.8.78 entry. ---