Skip to content

v0.8.82: UpdateLastAttempt audit tag + Step.05 Last Updated column + Step.08 attempts-without-run reconciliation#88

Merged
NeilBird merged 2 commits into
mainfrom
users/nebird/v0.8.82
Jun 15, 2026
Merged

v0.8.82: UpdateLastAttempt audit tag + Step.05 Last Updated column + Step.08 attempts-without-run reconciliation#88
NeilBird merged 2 commits into
mainfrom
users/nebird/v0.8.82

Conversation

@NeilBird

Copy link
Copy Markdown
Owner

Summary

Ships Item 5 from the post-v0.7.76 backlog: a per-cluster UpdateLastAttempt ARM tag stamped by Start-AzLocalClusterUpdate and an in-flight Step.08 reconciliation pass that compares those audit tags to the observed updateRun list. Also lands the Step.05 Last Updated readiness column + UpdateRing sort that was discussed alongside it.

What's new

Step.05 - readiness report

  • Get-AzLocalClusterUpdateReadiness now emits LastUpdated on every row shape (success / NotFound / Error), computed from the max packageVersions[].lastUpdated across all four package types.
  • Export-AzLocalClusterUpdateReadinessReport renders a new Last Updated column between Status and Recommended update, and sorts the detail table by UpdateRing -> Status priority -> ClusterName so each ring cohort stays grouped with its in-flight / remediation rows at the top.

UpdateLastAttempt audit tag (new)

  • Format: <UTC>;<Outcome>;<UpdateName>;<Reason> truncated to the 256-char Azure tag-value limit (reason truncated first with a trailing ~ sentinel).
  • Stamped by Start-AzLocalClusterUpdate at every outcome (HealthCheckBlocked, UpdateStarted, Failed). Writes swallow ARM errors and log a warning - the audit tag must NEVER block the apply flow.
  • Auto-cleared by Invoke-AzLocalSideloadedAutoResetForCluster (independent block, separate from the sideloaded-reset logic) when the latest update run is Succeeded AND either UpdateName matches the recorded attempt OR the attempt was a HealthCheckBlocked / Failed outcome more than 1h old.
  • New private helpers: Format-AzLocalUpdateLastAttemptTagValue, ConvertFrom-AzLocalUpdateLastAttemptTagValue, Write-AzLocalUpdateLastAttemptTag. New script-scoped constant = 'UpdateLastAttempt'.

Step.08 - in-flight monitor reconciliation

  • New Recent update attempts with no observable updateRun section. Joins the per-cluster UpdateLastAttempt tag against the observed updateRun list and surfaces any attempt within the last -RecentAttemptWindowHours (default 72) whose matching updateRun is missing or has a StartTimeUtc before attempt - 5min.
  • Captures URP package-internal pre-install health-check failures (cluster activity log shows Allows to apply updates: Succeeded but no updateRun resource is ever persisted - confirmed via ARG against Portland in the design pass: 10 updateRuns across 5 versions, none for the version recorded in its UpdateVersionInProgress tag).
  • Scope=by-update-ring path now ALWAYS calls Get-AzLocalClusterInventory -ScopeByUpdateRingTag so per-cluster tags are available for the reconciliation pass. The all-clusters path already had inventory. This is a behavior change vs. v0.8.81 (documented in CHANGELOG).
  • New parameter: [ValidateRange(0,8760)][int] = 72.
  • New pipeline output: attempts_without_run.
  • New -PassThru fields: AttemptWithoutRunCount + AttemptGaps.
  • Each gap emits a JUnit testcase with Type='AttemptWithoutRun', ClassName='UpdateMonitor'.

Tests

1248 tests, 0 failures (Pester v5.7.1 / Windows PowerShell 5.1).

New Describe blocks cover: Format / Convert tag helpers (round-trip, truncation, semicolon stripping, whitespace collapse, malformed -> ), the script-scoped tag-name constant, `LastUpdated` on all three row shapes, Step.05 column + sort, the three `Write-AzLocalUpdateLastAttemptTag` call sites in `Start-AzLocalClusterUpdate`, the auto-clear logic in `Invoke-AzLocalSideloadedAutoResetForCluster`, and Step.08 reconciliation (param, builder, 5-min slack, by-update-ring inventory, pipeline output, markdown section, JUnit Type, PassThru shape, empty-fleet PassThru).

Pre-existing Scope=by-update-ring test updated to assert Get-AzLocalClusterInventory IS called (was asserting it must NOT be called - intentional behavior change for the reconciliation pass).

Implementation notes

  • AttemptGaps in the -PassThru output uses .ToArray() rather than @(). Reason: @() around an EMPTY System.Collections.Generic.List[object] throws ArgumentException: Argument types do not match in Windows PowerShell 5.1 with no inner exception. .ToArray() returns T[0] cleanly. Saved to /memories/powershell-patterns.md.
  • All 22 ADO + GHA pipeline YAML templates have GENERATED_AGAINST_MODULE_VERSION bumped to '0.8.82'.
  • Module export count unchanged (60). No public-surface deletions.

Files touched

  • AzLocal.UpdateManagement.psd1 (ModuleVersion + 3 new Private dot-source entries)
  • AzLocal.UpdateManagement.psm1 (`` constant)
  • Private/Format-AzLocalUpdateLastAttemptTagValue.ps1 (new)
  • Private/ConvertFrom-AzLocalUpdateLastAttemptTagValue.ps1 (new)
  • Private/Write-AzLocalUpdateLastAttemptTag.ps1 (new)
  • Private/Invoke-AzLocalSideloadedAutoResetForCluster.ps1 (auto-clear block)
  • Public/Get-AzLocalClusterUpdateReadiness.ps1 (LastUpdated)
  • Public/Export-AzLocalClusterUpdateReadinessReport.ps1 (column + sort)
  • Public/Export-AzLocalUpdateRunMonitorReport.ps1 (reconciliation)
  • Public/Export-AzLocalFleetHealthStatusReport.ps1 (residual v0.8.81 polish)
  • Public/Start-AzLocalClusterUpdate.ps1 (3 tag-write call sites)
  • Tests/AzLocal.UpdateManagement.Tests.ps1 (+ Tests/test-run-timings.csv)
  • CHANGELOG.md / README.md / docs/release-history.md
  • 22 pipeline template YAMLs in Automation-Pipeline-Examples/

NeilBird added 2 commits June 15, 2026 13:20
…ers detail by Status priority

- Summary counts: each row reused the icon-map cell (which already includes its own label) AND appended a duplicate trailing label, producing 'Ready for Update Ready for update' / 'Up to Date Up to date' / 'Action Required Not ready for update' / 'Health Failure Clusters with Critical health failures'. Fixed by emitting the icon-map cell unmodified; the HealthFailure row keeps the 'Clusters with Critical health failures' qualifier in parentheses since it counts something different from the readiness cascade.
- All-clusters detail: previously sorted only by UpdateRing + ClusterName so operator-actionable rows were scattered. Now sorts by Status priority first (InProgress -> HealthFailure -> UpdateFailed -> ActionRequired -> SbeBlocked -> NeedsInvestigation -> ReadyForUpdate -> UpToDate), then UpdateRing + ClusterName as before. Up-to-Date clusters drop to the bottom; in-flight + remediation rows surface at the top.
- Pester: 1211 passed, 0 failed (no behavioural test relied on the duplicated labels or the prior sort order).
…column + Step.08 reconciliation

Three Item-5 deliverables shipped together, plus the residual v0.8.81 review polish.

Step.05 (Get-AzLocalClusterUpdateReadiness + Export-AzLocalClusterUpdateReadinessReport):
- New LastUpdated property on every output row, computed from max packageVersions[].lastUpdated across all packageTypes (Solution / Services / Platform / SBE).
- New 'Last Updated' column in the detail table between 'Status' and 'Recommended update'.
- Detail-table sort changed to UpdateRing -> Status priority -> ClusterName so ring cohorts stay grouped with in-flight/remediation rows at the top of each cohort.

UpdateLastAttempt cluster tag (new, owned by AzLocal.UpdateManagement):
- Format: <UTC>;<Outcome>;<UpdateName>;<Reason> truncated to the 256-char Azure tag-value limit (reason field truncated first with a trailing ~ sentinel).
- Stamped by Start-AzLocalClusterUpdate at every attempt outcome (HealthCheckBlocked, UpdateStarted, Failed). Writes swallow ARM errors and log Warning - the audit tag MUST NOT block the apply flow.
- Auto-cleared by Invoke-AzLocalSideloadedAutoResetForCluster (independent block, separate from sideloaded reset logic) when the latest update run is Succeeded AND either the UpdateName matches the recorded attempt OR the attempt was a HealthCheckBlocked/Failed outcome more than 1h old.
- New Private helpers: Format-AzLocalUpdateLastAttemptTagValue, ConvertFrom-AzLocalUpdateLastAttemptTagValue, Write-AzLocalUpdateLastAttemptTag. New script-scoped constant $script:UpdateLastAttemptTagName = 'UpdateLastAttempt'.

Step.08 (Export-AzLocalUpdateRunMonitorReport):
- New 'Recent update attempts with no observable updateRun' section. Joins the per-cluster UpdateLastAttempt tag against the observed updateRun list and surfaces any attempt within the last -RecentAttemptWindowHours (default 72) whose matching updateRun is missing or has a StartTimeUtc before (attempt-5min).
- Captures Portland-style URP package-internal pre-install health-check failures (cluster activity log shows 'Allows to apply updates: Succeeded' but no updateRun resource is ever persisted - confirmed via ARG on 2026-06-15: Portland has 10 updateRuns across 5 versions, none for Solution12.2605.1003.210 which is in its UpdateVersionInProgress tag).
- by-update-ring scope path now ALWAYS calls Get-AzLocalClusterInventory (with -ScopeByUpdateRingTag) so per-cluster tags are available for the reconciliation pass. The all-clusters path already had inventory.
- New pipeline output: attempts_without_run.
- New PassThru fields: AttemptWithoutRunCount + AttemptGaps (uses .ToArray() not @(...) - empty Generic.List[object] wrap throws ArgumentException in PS 5.1).
- New parameter: [ValidateRange(0,8760)][int] = 72.
- Each gap also emits a JUnit testcase with Type='AttemptWithoutRun', ClassName='UpdateMonitor'.

Tests:
- Pester suite expanded: 1248 tests, 0 failures. New Describe blocks cover Format/Convert tag helpers (round-trip, truncation, semicolon stripping, whitespace collapse, malformed-input -> ), the script-scoped tag-name constant, Get-AzLocalClusterUpdateReadiness LastUpdated presence on all shapes (success / NotFound / Error), Step.05 column + sort, Start-AzLocalClusterUpdate three call sites, Invoke-AzLocalSideloadedAutoResetForCluster clear logic, and Step.08 reconciliation (param,  builder, 5-min slack, by-update-ring inventory, pipeline output, markdown section, JUnit Type, PassThru shape, empty-fleet PassThru).
- Pre-existing Scope=by-update-ring test updated: now asserts Get-AzLocalClusterInventory IS called with -ScopeByUpdateRingTag (was asserting it must NOT be called - intentional behavior change for the reconciliation pass).

CHANGELOG + docs/release-history.md + README.md updated. Pipeline YAML GENERATED_AGAINST_MODULE_VERSION pins bumped to '0.8.82' across all 22 ADO + GHA workflow templates. Module export count unchanged (60). Resolves: ship Item-5 backlog from /memories/repo/AzLocal.UpdateManagement-post-v0.7.76-backlog.md.
@NeilBird
NeilBird merged commit c68335b into main Jun 15, 2026
2 checks passed
@NeilBird
NeilBird deleted the users/nebird/v0.8.82 branch June 15, 2026 13:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant