AzLocal.UpdateManagement v0.8.5: thin-YAML port across all 10 Step pipelines (35 → 55 cmdlets, zero UI delta)#74
Merged
Merged
Conversation
…ip line
Two Pester drift-test issues found by full-suite execution:
1. ADO Step.6 banner test expected 2 emits per file (one per install step).
The second install step (ApplyUpdates stage) deliberately skips
##vso[task.uploadsummary] - there is a comment in the yml explaining
that this avoids duplicating the banner in the rendered Summary. Correct
the expectation to 1 per yml (10 ADO Summary emits total).
2. RBAC bare-name test flagged the migration-tip blockquote in
Automation-Pipeline-Examples/README.md that intentionally quotes
"Name": "Azure Stack HCI Update Operator" to explain the rename.
Allowlist lines that contain "pre-v0.8.4" - that marker is exclusive
to explicit migration documentation.
Also correct the stale "11 ADO install sites" claim in:
- AzLocal.UpdateManagement/README.md (What's New in v0.8.4)
- AzLocal.UpdateManagement/CHANGELOG.md (v0.8.4 section, 2 places)
- AzLocal.UpdateManagement/AzLocal.UpdateManagement.psd1 (ReleaseNotes)
Accurate description: one banner per yml across all 20 bundled Step.{0..9}.yml
templates (Step.6 ADO has 2 install steps but the second deliberately skips
upload to avoid duplicating the banner).
Drift subset re-run: 8 Passed, 0 Failed.
Foundation cmdlet for the v0.8.5 thin-YAML refactor (roadmap commit 2/13). Condenses the ~50-line install/drift/banner block from every Step.*.yml into a single cmdlet call: resolves installed module version, looks up latest on PSGallery via Find-Module (skippable), emits up to three drift annotations via Phase 0 pipeline-host helpers, appends a one-line markdown banner to the rendered step summary, and emits three step outputs (installed_module_version / generated_against_version / latest_on_psgallery). Host detection (GitHub Actions / Azure DevOps / Local) flows through the existing Get-AzLocalPipelineHost / Set-AzLocalPipelineOutput / Add-AzLocalPipelineStepSummary / Write-AzLocalPipelineNotice / Write-AzLocalPipelineWarning helpers - no per-host branching inside the cmdlet itself. Exported via BOTH psd1 FunctionsToExport AND psm1 Export-ModuleMember (psm1 has an explicit allow-list, not auto-discover - psd1-only changes are insufficient). Tests: 15 new It blocks covering ValidatePattern, in-sync verdict, pinned vs latest pin-status, three host-mode step outputs, GH step-summary banner regex, YAML-newer WARNING annotation, YAML-older NOTICE annotation, Find-Module failure swallowed, newer-on-PSGallery NOTICE + verdict. Full suite: 934 passed / 0 failed / 1 skipped.
…ve_for_date_utc) GH workflow_dispatch.inputs gains 'use_schedule_file' (choice false/true, default false) + 'resolve_for_date_utc' (string, YYYY-MM-DD). ADO parameters gains symmetric 'useScheduleFile' (boolean) + 'resolveForDateUtc' (string). Lets an operator trigger Step.6 manually but resolve UpdateRing + AllowedUpdateVersions from apply-updates-schedule.yml exactly as a scheduled run would. Use cases: (a) test a schedule change before the next scheduled tick, (b) re-run a missed scheduled day, (c) preview a future cycleWeek / dayOfWeek via resolve_for_date_utc. Resolver decision tree (both platforms): 1. Manual + use_schedule_file=false -> manual ring verbatim (v0.8.4 back-compat) 2. Manual + use_schedule_file=true -> read schedule, resolve for today UTC (or supplied date) 3. Manual + use_schedule_file=false + empty update_ring -> throw with remediation 4. Scheduled trigger -> unchanged (resolve schedule for UTC now) update_ring / updateRing is no longer required:true (paired with the use_schedule_file=true escape hatch). resolve_for_date_utc is parsed via [datetime]::ParseExact 'yyyy-MM-dd' with AssumeUniversal | AdjustToUniversal so the date is treated as UTC regardless of agent timezone. Pester guard already shipped: 12 It blocks under Context 'v0.8.5 Step.6 manual schedule-file input' cover both yml declarations + resolver script env-var wiring + the both-empty throw + the yyyy-MM-dd parsing path. Suite 934/0/1.
Full v0.8.5 release notes covering:
1. New Public cmdlet Get-AzLocalApplyUpdatesScheduleCycleCalendar
(calendar projection of apply-updates-schedule.yml across one full
cycle - default CycleWeeks * 7 days - structured object pipeline
or rendered markdown via -AsMarkdown).
2. New Public cmdlet Add-AzLocalPipelineVersionBanner (thin-YAML
foundation - condenses ~50 lines of install/drift/banner per yml).
3. New Step.6 manual schedule-file inputs (use_schedule_file +
resolve_for_date_utc on both GH and ADO).
4. Regression fix: v0.8.4 cycle calendar silently dropped on healthy
fleets (architectural decoupling from advisor findings gate).
5. New per-day Clusters-in-ring(s) column + per-ring Cluster-count
column (-ClusterRingCounts hashtable on the new cmdlet).
6. New optional ### Per-ring projection section
(-IncludePerRingSummary on the new cmdlet).
7. Test-AzLocalApplyUpdatesScheduleCoverage -View Recommend now
delegates Enhancement B to the new cmdlet.
8. GENERATED_AGAINST_MODULE_VERSION 0.8.4 -> 0.8.5 across all 20
bundled Step.{0..9}.yml templates.
No public API removed. No parameter changes on existing cmdlets.
…ared JUnit emitter
- New Public cmdlet Export-AzLocalAuthValidationReport replaces the ~200-line
inline run: | block in Step.0_authentication-test.yml (GH + ADO). Runs the
four auth/RBAC/ARG probes (az account show, az role assignment list,
az account list, Resource Graph cluster reachability), writes
auth-report.xml (JUnit), subscriptions.json, subscriptions.csv, emits the
markdown summary, and produces three pipeline step outputs
(subscription_count, cluster_count, auth_valid).
- New Private helper New-AzLocalPipelineJUnitXml provides a single shared
JUnit XML emitter so every Step.* cmdlet produces consistent, well-formed
testsuites/testcase output. Includes XML-escaping of attribute values and
UTF-8 no-BOM output.
- Step.0 install step now calls Add-AzLocalPipelineVersionBanner (Public)
in place of the ~50-line inline install/drift/banner block. ADO variant
uses -PassThru to forward the version triple to job-scoped variables for
the report step to consume.
- 41 functions exported (was 40).
- Tests:
* +9 It blocks for New-AzLocalPipelineJUnitXml shape/escaping
* +10 It blocks for Export-AzLocalAuthValidationReport behaviour
* Banner-emit guard updated to accept Add-AzLocalPipelineVersionBanner
calls (alongside the legacy inline literal) and ignore the cmdlet name
when it appears only in a YAML comment line
* Drift-guard test (installed-older-than-generated, v0.7.66) now treats
Add-AzLocalPipelineVersionBanner as an equivalent emitter (the cmdlet
runs the installed -lt generated comparison internally and raises the
warning via Write-AzLocalPipelineWarning)
- Pester: 952 passed / 0 failed / 1 skipped (3 min 16 s).
- New Public cmdlet collapses ~250-line inline run: | blocks in Step.1_inventory-clusters.yml (GH + ADO) into a single call. - Calls Get-AzLocalClusterInventory once (was twice in v0.8.4 yml), writes timestamped + canonical CSV, JSON, README, and step summary. - Emits 4 pipeline outputs via Set-AzLocalPipelineOutput (cluster_count, with_tag_count, without_tag_count, csv_path). - Registered in psd1.NestedModules + psd1.FunctionsToExport + psm1.Export-ModuleMember; module now exports 42 functions (was 41). - 8 new unit tests cover PassThru shape, artifact writes, empty fleet, GH + ADO host outputs, SubscriptionFilter -> -SubscriptionId pass-through, UpdateRing distribution sorting, single-cluster shape. - Pester: 924 passed / 0 failed / 1 skipped.
Collapses ~250 lines of inline run/script blocks across the GitHub Actions and Azure DevOps Step.2_manage-updatering-tags.yml pipelines into a single Public cmdlet call. The cmdlet: - Reuses Set-AzLocalClusterUpdateRingTag for the actual per-cluster tag write (no duplication of Azure CLI tag logic). - Validates the CSV up front (existence, required ResourceId/UpdateRing columns, non-empty UpdateRing column) and emits the full TWO-STAGE operator recovery guidance via Write-Host on a missing file. - Writes UpdateRingTag_Results.json sidecar into -OutputDirectory. - Emits 8 pipeline outputs via Set-AzLocalPipelineOutput: total_count, created_count, updated_count, already_in_sync_count, skipped_count, failed_count, whatif_count, results_json_path. - Emits a markdown step summary (Settings table + Result breakdown + collapsible per-cluster details) via Add-AzLocalPipelineStepSummary. - Honours -WhatIf via $WhatIfPreference and -Force via [switch] propagation. YAML reductions: - GitHub Actions Step.2: 417 → 214 lines (49% reduction). - Azure DevOps Step.2: 376 → 165 lines (56% reduction). Install task now calls Add-AzLocalPipelineVersionBanner (Public). Apply task collapses to a single Set-AzLocalClusterUpdateRingTagFromCsv call wired to the banner step's installed_module_version output for the run-footer attribution line. Module: psd1 NestedModules + FunctionsToExport + psm1 Export-ModuleMember updated. Module now exports 43 functions (was 42). Tests: +11 Pester tests under Tests/AzLocal.UpdateManagement.Tests.ps1 #region v0.8.5: Set-AzLocalClusterUpdateRingTagFromCsv (Step.2 thin-YAML port) covering CSV-missing fail-fast, column validation, empty-results JSON sidecar, tally counts (Created/Updated/AlreadyInSync/Skipped/Failed/WhatIf), PassThru shape, default OutputDirectory selection, and WhatIfPreference propagation. BS7 "exactly 42 functions" assertions bumped to 43. Validated: Full Pester suite — Passed=935 Failed=0 Skipped=1 Total=936 Duration=00:01:24.
Collapses ~960 lines of inline run/script blocks across the GitHub
Actions and Azure DevOps Step.7_monitor-updates.yml pipelines into
a single Public cmdlet call. The cmdlet:
- Reuses Get-AzLocalClusterInventory + Get-AzLocalUpdateRuns
(with -PassThru -SkipSideloadedReset) for the fleet query in both
scopes ('all' via inventory, 'by-update-ring' via tag).
- Reuses New-AzLocalPipelineJUnitXml (Private), Set-AzLocalPipelineOutput,
Add-AzLocalPipelineStepSummary and Get-AzLocalPipelineHost - the same
thin-YAML primitives Step.2/8/9 already use.
- Classifies each run row with elapsed durations, per-step warn/crit
thresholds (default 2h/4h), overall warn/crit/skull thresholds
(default 24h / 3d / 6d), step-error signal (progressStatus='Error'
while State='InProgress'), recent-failure window (default 24h),
unresolved-failure flag, severity score, and portal URLs.
- Writes update-monitor.csv (sorted by SeverityScore desc) and
update-monitor.xml (JUnit; one <testcase> per in-flight run + one
per unresolved-failed run) into -OutputDirectory.
- Emits 6 step outputs via Set-AzLocalPipelineOutput: in_flight,
long_running, long_running_step, step_errored, recent_failures,
unresolved_failures (all lowercase snake_case to match Step.2/8/9
contract; lowercase is REQUIRED on ADO and case-insensitive on GH).
- Emits markdown step summary (status badge + scope/threshold line +
metric table + 'In-flight runs' + 'Failed runs (unresolved)'
tables + action-required / healthy footer) via Add-AzLocalPipelineStepSummary.
- Honours -Now for deterministic testing; -PassThru returns counts,
artifact paths, and the enriched Rows array.
YAML reductions:
- GitHub Actions Step.7: 663 -> 225 lines (66% reduction).
Install step uses Add-AzLocalPipelineVersionBanner (Public).
jobs.monitor.outputs map lowercased to match new step outputs.
- Azure DevOps Step.7: 649 -> 187 lines (71% reduction).
Install task uses Add-AzLocalPipelineVersionBanner (Public).
Snapshot task collapses to a single Export-AzLocalUpdateRunMonitorReport
call wired to the banner step's installed_module_version output.
Module: psd1 NestedModules + FunctionsToExport + psm1 Export-ModuleMember
updated. Module now exports 44 functions (was 43).
Tests: +11 Pester tests in Tests/AzLocal.UpdateManagement.Tests.ps1
#region v0.8.5: Export-AzLocalUpdateRunMonitorReport (Step.7 thin-YAML port)
covering:
1. Empty fleet -> all-zero outputs + idle status badge.
2. Single in-flight within thresholds -> HEALTHY badge, no chips.
3. Per-step warn: step elapsed > LongRunningStepHours -> warn chip
and LongRunningStepCount=1.
4. Step error (progressStatus=Error, State=InProgress) -> StepErroredCount=1
and elevated SeverityScore + JUnit 'StepError' failure type.
5. Unresolved failure (State=Failed) -> UnresolvedFailureCount=1 +
JUnit 'RecentFailure' failure type.
6. RecentFailureWindowHours=0 disables recent flag but unresolved
still surfaces.
7. Scope='by-update-ring' skips Get-AzLocalClusterInventory and
queries by tag (Should -Invoke ... -Times 0 -Exactly).
8. CSV is sorted by SeverityScore descending (worst first).
9. JUnit XML is well-formed; one testcase per in-flight + per
unresolved failure.
10. Markdown step summary contains required headings, metric table,
and footer module version.
11. Defaults OutputDirectory to BUILD_ARTIFACTSTAGINGDIRECTORY when
on Azure DevOps host.
Function-count assertions bumped 43 -> 44.
Validated: Full Pester suite -> Passed=946 Failed=0 Skipped=1
Total=947 Duration=00:00:57.
Port Step.8 fleet update status pipeline (GitHub Actions + Azure DevOps) to the thin-YAML pattern. The fat ~1,200-line inline run: blocks are condensed to a single Public cmdlet invocation per platform. Module changes - Public/Export-AzLocalFleetUpdateStatusReport.ps1: new 1,024-line cmdlet with 3 parameter sets (all clusters / by-update-ring / file overrides), 3 JUnit testsuites (Fleet Version Distribution, AzureLocalFleetUpdateStatus, Update Run History and Error Details), markdown step summary, PassThru with 35 properties. Registered in psd1/psm1. - Private/New-AzLocalPipelineJUnitXml.ps1: extended JUnit emitter to surface <properties><property name=... value=... /></properties> at both suite and testcase levels, with XML-escaping for special chars. Pipeline porting - Automation-Pipeline-Examples/github-actions/Step.8_fleet-update-status.yml 1,217 -> 332 lines - Automation-Pipeline-Examples/azure-devops/Step.8_fleet-update-status.yml 1,210 -> 324 lines Tests - 12 new Step.8 cmdlet tests via InModuleScope + Mock fixtures (priority cascade, manifest fallback, IncludeUpdateRuns gating, ADO output dir resolution, by-update-ring parameter forwarding, PassThru shape). - 3 new JUnit Properties helper tests (suite/testcase emit, XML-escape). - Removed 3 stale fat-yml-content Describe blocks (132 lines) whose assertions were string-matches against the old inline run: blocks. - BS7 expected function count: 44 -> 45. Pester: 951 passed / 0 failed / 1 skipped / 59s.
Port Step.5_assess-update-readiness.yml (GH + ADO) to the thin-YAML pattern. The ~280-line inline 'Run readiness + blocking health checks' block is now the Export-AzLocalClusterUpdateReadinessReport Public cmdlet.
Cmdlet behavior (mirrors v0.8.4 yml byte-for-byte):
* Resolves OutputDirectory ('./artifacts' on GH/Local; BUILD_ARTIFACTSTAGINGDIRECTORY on ADO).
* Get-AzLocalClusterInventory -PassThru for ResourceId -> UpdateRing map.
* Scope=all + empty inventory short-circuits with IDLE markdown + zero outputs.
* Get-AzLocalClusterUpdateReadiness called twice (CSV + JUnit XML) to preserve the dorny/test-reporter contract.
* Test-AzLocalClusterHealth -BlockingOnly called twice (CSV + JUnit XML).
* 3-bucket model: ReadyForUpdate / UpToDate / NotReady.
* CriticalCount sum + clustersWithCritical count from PassThru rows.
* Merges readiness.xml + health-blocking.xml into assess-readiness.xml under <testsuites name='Update Readiness Assessment'>.
* Emits 8-section markdown step summary (header tile, action banner, summary counts, Not-Ready table, Critical-health table, per-UpdateRing pivot, all-clusters detail, cross-links).
* Emits 2 step outputs via Set-AzLocalPipelineOutput: not_ready, critical_failures (lowercase per v0.8.5 convention).
YAML shrink:
* github-actions/Step.5_assess-update-readiness.yml: 487 -> 213 lines.
* azure-devops/Step.5_assess-update-readiness.yml: 454 -> 195 lines.
* Install step uses Add-AzLocalPipelineVersionBanner (same as Steps 0/1/2/7/8).
* Job-level outputs renamed NOT_READY/CRITICAL_FAILURES -> not_ready/critical_failures to match cmdlet emit.
Registration:
* psd1: NestedModules + FunctionsToExport.
* psm1: Export-ModuleMember allowlist.
* Tests.ps1: BS7 expected function count 45 -> 46, expectedFunctions list.
Tests added (Describe 'Thin-YAML Step.5: Export-AzLocalClusterUpdateReadinessReport', 9 cases):
* Empty inventory short-circuit (IDLE markdown + zero outputs + PassThru zero counts).
* Scope=all forwards ClusterResourceIds.
* Scope=by-update-ring forwards ScopeByUpdateRingTag + UpdateRingValue.
* 3-bucket counts (1 ready, 1 uptodate, 1 notready) + criticalFindings sum.
* Combined XML merge under 'Update Readiness Assessment'.
* PassThru exposes all 13 documented properties.
* ADO host resolves OutputDirectory to BUILD_ARTIFACTSTAGINGDIRECTORY.
* Per-UpdateRing pivot section when >1 ring in scope.
* OK header tile when all clusters ready and zero Critical.
Pester: 960 passed / 0 failed / 1 skipped.
New cmdlet wraps the ~255-line "Collect Fleet Connectivity Data" inline
block in both Step.4_fleet-connectivity-status.yml platform files.
Reuses existing helpers:
- Get-AzLocalFleetConnectivityStatus -ExportPath -PassThru (data collection)
- New-AzLocalFleetConnectivityStatusSummary -FromObjects (no CSV round-trip)
- New-AzLocalPipelineJUnitXml (shared XML emitter, Properties param)
- Set-AzLocalPipelineOutput / Add-AzLocalPipelineStepSummary
- Add-AzLocalPipelineVersionBanner / Get-AzLocalPipelineHost
YAML changes:
- Step.4 GH and ADO workflows now invoke the cmdlet and emit 12 lowercase
snake_case step outputs (total_clusters, connected_clusters,
disconnected_clusters, total_arc_machines, connected_arc_machines,
not_connected_arc_machines, expired_arc_machines, total_nics,
total_arbs, online_arbs, offline_arbs, critical_count).
- ITSM downstream blocks preserved unchanged on both platforms.
- Step.4 BS guard rewritten for v0.8.5 (cmdlet-only smoke check).
Renderer hardening (New-AzLocalFleetConnectivityStatusSummary):
- FromCsvReports branch: rewrote ` = if (Test-Path) { @() } else { @() }`
to plain assignment form. Under PS 5.1 strict mode + InModuleScope the
if-as-expression silently left ` = `, causing
`@( | Where-Object { .ClusterId })` to throw
"property 'ClusterId' cannot be found".
- Cluster node-count rollup: replaced `Measure-Object -Property NodeCount
-Sum` (throws under strict mode when the pipeline is empty or rows
lack the property) with an explicit foreach loop that probes
`.PSObject.Properties['NodeCount']` + `[int]::TryParse`.
Tests:
- 10 new Pester It blocks under "Thin-YAML Step.4:
Export-AzLocalFleetConnectivityStatusReport".
- Test data pscustomobject literals now include NodeCount / Location /
SubscriptionId / ResourceGroup so the renderer's table-building code
paths (`.NodeCount` etc.) don't strict-mode-fault on minimal rows.
Module manifest + .psm1:
- NestedModules and FunctionsToExport include
Export-AzLocalFleetConnectivityStatusReport.
- Export-ModuleMember allowlist updated.
Also fixes the v0.8.5 Step.5 Combined-XML test:
- Pre-creates readiness.xml and health-blocking.xml inside the test
BeforeEach (Pester Mock bodies cannot reliably write to ``
inside `InModuleScope` because Pester rebinds parameter scopes).
- Mocks now just return the row arrays; the cmdlet's merge step reads
the pre-staged XML files.
Test summary: Passed=970 Failed=0 Skipped=1.
Condenses ~430 inline lines across the GitHub Actions and Azure DevOps Step.3_apply-updates-schedule-audit.yml scaffolds into a single Public cmdlet that orchestrates the existing schedule-coverage advisor and calendar helpers and writes both the 4 ./reports/schedule-coverage-* artifacts and the 12 lowercase step outputs. Shared helpers reused (no behaviour duplication): - Get-AzLocalPipelineHost, Set-AzLocalPipelineOutput - Add-AzLocalPipelineStepSummary, Add-AzLocalPipelineVersionBanner - New-AzLocalPipelineJUnitXml (multi-suite via -Suites) - Test-AzLocalApplyUpdatesScheduleCoverage (-View Audit/Matrix/Recommend) - Get-AzLocalApplyUpdatesScheduleConfig (.SchemaVersion, allow-list shape) - Get-AzLocalApplyUpdatesScheduleCycleCalendar (-AsMarkdown -IncludePerRingSummary) Fixes v0.8.4 Cycle Calendar silent-drop regression: the calendar now renders UNCONDITIONALLY whenever -SchedulePath is supplied, eliminating the v0.8.4 hasIssues-gate that silently dropped the calendar on clean-fleet runs. Also fixes a silent format-string bug pattern discovered during testing: .Add(fmt -f a, b, c) inside a method call resolves the commas as ARGUMENT SEPARATORS for the method, not as the -f operator arglist - so -f only ever sees the first argument and throws "Index ... must be ... less than the size of the argument list". Wrap the -f expression in an extra paren pair when used inside a method invocation. Saved to user memory for future sessions. 11 new Pester tests in Thin-YAML Step.3: Export-AzLocalApplyUpdatesScheduleAudit Describe block covering schema v1 vs v2 allow-list rendering, cycle calendar unconditional render, zero-row JUnit placeholder testcase, hasIssues recommendation prepending, dual JUnit suite emission (Schedule + Cron), schema v2 per-row allow-list, sched-path arg passthrough, output count, and step-output emission. Pre-existing yaml-validator tests rewritten to assert against the cmdlet source (content moved during the thin-YAML refactor). Function count 47 -> 48. Tests: 979 passed, 0 failed, 1 skipped (Pester v5.7.1).
Adds a regression Pester test under "Thin-YAML Step.7:
Export-AzLocalUpdateRunMonitorReport" that asserts both the Cluster
cell and the Update cell of the "In-flight runs (sorted by severity
score, worst first)" markdown table are rendered as
<a href="https://portal.azure.com/..." target="_blank" rel="noopener">name</a>
so operators clicking through to the portal blade from a pipeline
step summary keep the pipeline tab open and do NOT lose their place
in the workflow run.
The cmdlet already emits target=_blank rel=noopener on lines 557-558
(in-flight table) and 583-584 (Failed runs table) since the v0.8.5
Step.7 port in commit d8c019f - this test simply guards against any
future refactor silently dropping the attribute. No production code
change.
Tests: 12 passed, 0 failed in the Step.7 subset.
Condenses ~600 lines of inline `run: |` PowerShell from each Step.9
fleet-health-status pipeline (GitHub Actions + Azure DevOps) into a
single Public cmdlet, applying the v0.8.5 thin-YAML pattern already in
use for Step.0/1/2/3/4/5/7/8.
New Public cmdlet
-----------------
Public/Export-AzLocalFleetHealthStatusReport.ps1
- Queries Get-AzLocalFleetHealthFailures -View Detail (one ARG call,
optionally filtered by -Severity and -UpdateRingTag).
- Builds a SUMMARY roll-up in process via Group-Object FailureReason +
Severity. Sort order: Critical-first, then ClusterCount desc, then
FailureCount desc.
- Queries Get-AzLocalFleetHealthOverview for the per-cluster rollup.
- Writes the standard CSV+JSON artefact bundle: detail / summary /
overview pairs, plus the JUnit XML and the markdown step summary.
- Emits a 2-suite JUnit XML ([JUnit Debug] Critical Health Failures /
[JUnit Debug] Warning Health Failures) via the shared
New-AzLocalPipelineJUnitXml helper. Each <testcase> carries the ITSM
dedupe properties (ClusterResourceId / UpdateName=FailureReason /
Status=Severity / Severity / ClusterPortalUrl / TargetResourceName /
TargetResourceType / FailureReason) consumed by New-AzLocalIncident.
- Renders a 4-section markdown step summary via the shared
Add-AzLocalPipelineStepSummary helper: KPI table / Fleet Health
Overview (top 100 clusters with target=_blank portal-link hyperlinks
and Unicode health glyphs) / Health Check Failures By Reason (top
25 reasons, zipped portal-link hyperlinks) / Detailed Results
(per-cluster collapsible <details>, capped at 100 clusters).
- Emits 8 lowercase snake_case step outputs via Set-AzLocalPipelineOutput:
total_clusters, total_failures, critical_count, warning_count,
distinct_reasons, overview_rows, healthy_clusters, total_in_sub.
- -PassThru returns a PSCustomObject with all bucket counts + file
paths + DetailRows + SummaryRows + OverviewRowsData for unit tests
and ad-hoc PowerShell callers.
Thin pipeline YAMLs
-------------------
Step.9 GitHub Actions: 758 -> 343 lines (-415 lines).
Step.9 Azure DevOps : 753 -> 336 lines (-417 lines).
Both YAMLs now consist of:
1. Install + drift-detection banner via Add-AzLocalPipelineVersionBanner
2. Single Collect Fleet Health Status step that calls
Export-AzLocalFleetHealthStatusReport with Scope / Severity /
UpdateRing + InstalledModuleVersion
3. Compute Artifact Timestamp
4. Upload reports (90d retention on GH; PublishBuildArtifacts on ADO)
5. Publish JUnit Diagnostic Results
6. ITSM (unchanged from prior version)
The previous inline "Create Fleet Health Summary" / "Display Fleet
Health Summary" markdown-emission step is no longer needed - the
cmdlet writes to $GITHUB_STEP_SUMMARY (GH) / emits
##vso[task.uploadsummary] (ADO) via Add-AzLocalPipelineStepSummary.
Module registration
-------------------
- AzLocal.UpdateManagement.psd1: NestedModules + FunctionsToExport
- AzLocal.UpdateManagement.psm1: Export-ModuleMember
Tests
-----
- New Describe "Thin-YAML Step.9: Export-AzLocalFleetHealthStatusReport"
(6 It blocks):
* empty fleet -> zero counts, zero-valued step outputs,
placeholder JUnit testcase
* Critical+Warning mixed fleet -> correct bucket counts,
both severity suites in JUnit, ITSM <properties> present,
target=_blank portal-link hyperlinks in the markdown summary
* Scope by-update-ring forwards UpdateRing as -UpdateRingTag to
both source cmdlets (Should -Invoke -ParameterFilter)
* Severity filter forwards to Get-AzLocalFleetHealthFailures
* Azure DevOps host defaults OutputDirectory to
BUILD_ARTIFACTSTAGINGDIRECTORY\reports
* Summary view orders Critical-first then ClusterCount desc
- Bumped "Should export exactly 48 functions" -> 49 (both sites in
the test file) so the manifest-load Describe block passes.
Test results
------------
- Thin-YAML Step.9 Describe: 6 passed, 0 failed
- Module: AzLocal.UpdateManagement Describe: 42 passed, 0 failed
Condenses the inline `run: |` PowerShell from each Step.6
readiness-gated apply-updates pipeline (GitHub Actions + Azure DevOps)
into six new Public cmdlets, applying the v0.8.5 thin-YAML pattern
already in use for Step.0/1/2/3/4/5/7/8/9.
New Public cmdlets (6)
----------------------
Public/Resolve-AzLocalPipelineUpdateRing.ps1
- Owns the manual-vs-schedule-file ring resolution previously
duplicated as ~80 lines of inline script in each pipeline.
- Back-compat: returns ManualUpdateRing verbatim when -UseScheduleFile
is not set; throws on empty manual ring + no schedule file.
- Schedule-file path: parses YAML schedule via
Get-AzLocalApplyUpdatesScheduleConfig, then calls
Resolve-AzLocalCurrentUpdateRing -Schedule $cfg -Now $resolveAt
where $resolveAt honours -ResolveForDateUtc (yyyy-MM-dd).
- Emits RESOLVED_UPDATE_RING and RESOLVED_ALLOWED_UPDATE_VERSIONS
as step outputs (same name on both hosts; this is intentional -
consumed via $env:RESOLVED_UPDATE_RING in the next step on both).
- No-row case uses per-host severity (GH=notice, ADO=warning).
Public/Export-AzLocalClusterReadinessGateReport.ps1
- Owns the readiness scan + CSV/MD generation previously
duplicated as ~250 lines of inline script.
- Empty-ring short-circuit: emits zero counters, no scan.
- Per-host step-output naming:
* GH: READY_COUNT / TOTAL_COUNT / NOT_READY_COUNT (UPPER_SNAKE)
* ADO: ReadyCount / TotalCount / NotReadyCount (PascalCase) so
the ADO YAML stageDependencies binding
outputs[`readiness.ReadyCount`] resolves correctly.
Public/Invoke-AzLocalReadinessGatedClusterUpdate.ps1
- Owns the per-cluster apply loop + JUnit XML + apply-results.json
generation previously duplicated as ~300 lines of inline script.
- Throws when the readiness CSV is missing the ClusterResourceId
column (v0.7.62 contract preserved).
- Per-host step-output naming:
* GH: SUCCEEDED / SKIPPED / FAILED / HEALTH_BLOCKED /
SCHEDULE_BLOCKED / SIDELOADED_BLOCKED / EXCLUDED_BY_TAG
* ADO: Succeeded / Skipped / Failed / HealthBlocked /
ScheduleBlocked / SideloadedBlocked / ExcludedByTag
- The seven-counter emitter is a regular scriptblock (NOT
.GetNewClosure(), which would sever access to the private
Set-AzLocalPipelineOutput function via SessionState).
Public/Add-AzLocalApplyUpdatesStepSummary.ps1
- Owns the markdown apply-updates rollup previously emitted by
~150 lines of inline script.
- Host-aware icons: Unicode on GH; GitHub-Markdown shortcodes on ADO.
Public/Add-AzLocalNoReadyClustersStepSummary.ps1
- Owns the no-clusters-ready notice job summary previously
emitted by ~40 lines of inline script.
Public/Invoke-AzLocalItsmTicketingFromArtifact.ps1
- Owns the post-apply ITSM ticket emission previously duplicated
as ~150 lines of inline script.
- Short-circuits silently when ConfigPath or InputArtifactPath
is missing (consumer ITSM config is opt-in).
Thin pipeline YAMLs
-------------------
Step.6 GitHub Actions: 964 -> ~480 lines (-484 lines).
Step.6 Azure DevOps : 985 -> ~440 lines (-545 lines).
Both YAMLs now consist of:
1. Install + drift-detection banner via Add-AzLocalPipelineVersionBanner
2. Single Resolve Update Ring step -> Resolve-AzLocalPipelineUpdateRing
3. Single Check Readiness step -> Export-AzLocalClusterReadinessGateReport
4. Compute Artifact Timestamp
5. Upload readiness-report artifact
6. Single Apply Updates step -> Invoke-AzLocalReadinessGatedClusterUpdate
7. Single Summary step -> Add-AzLocalApplyUpdatesStepSummary
8. ITSM (Invoke-AzLocalItsmTicketingFromArtifact)
9. No-clusters-ready job -> Add-AzLocalNoReadyClustersStepSummary
The pipeline UI / step outputs / artifact contents / summary text are
unchanged byte-for-byte (per-host naming/icons are preserved).
Module registration
-------------------
- AzLocal.UpdateManagement.psd1: NestedModules + FunctionsToExport
bumped from 49 to 55.
- AzLocal.UpdateManagement.psm1: Export-ModuleMember updated.
Tests
-----
- Bumped "Should export exactly 49 functions" -> 55 (both sites).
- Appended the 6 new cmdlet names to expectedFunctions.
- Replaced 4 broken inline-script-grep It blocks in the existing
"v0.8.4 Step.6 Enhancement D" Context with thin-cmdlet invocation
guards under a renamed "v0.8.5 Step.6 thin-YAML" Context.
- Replaced 6 broken inline-script-grep It blocks in the
"v0.8.5 Step.6 manual schedule-file input" Context with
thin-cmdlet invocation guards for Resolve-AzLocalPipelineUpdateRing
on GH + ADO.
- New Describe blocks (parameter shape + per-host short-circuit guards)
for all 6 new Public cmdlets.
Test results
------------
- Pester suite: 1069 passed, 0 failed, 1 skipped (~4m38s).
CHANGELOG.md (v0.8.5 entry)
---------------------------
- Reworded lead to reflect the full scope: original 0.8.5 Step.3
cycle-calendar refactor PLUS the subsequent thin-YAML port across
all 10 Step pipelines (Step.0 - Step.9, GH + ADO = 20 YAML files).
- Updated total export count: 35 (v0.8.4) -> 55 (v0.8.5).
- New section "Thin-YAML refactor across all 10 Step pipelines":
- Goal: condense duplicated inline run: blocks into single Public
cmdlets shared between GH + ADO.
- Lists all 14 new Public cmdlets by Step (Step.6 has 6, others 1).
- Documents per-host step-output naming (UPPER_SNAKE on GH,
PascalCase on ADO for stageDependencies bindings).
- Documents per-host icon style (Unicode on GH, GitHub-Markdown
shortcodes on ADO).
- Quantifies the line reduction (~9,000 lines removed across 20
templates; Step.6 alone -484 lines GH / -545 lines ADO).
- Calls out maintenance benefits: single source of truth,
unit-testable, faster CI signal, cleaner diffs, discoverability
via Get-Help, reusable shared helpers.
- Documents the .GetNewClosure() / SessionState bug shaken out
during testing and how it was fixed.
- Records the final Pester baseline: 1069 passed, 0 failed,
1 skipped, ~4m38s.
- Confirms full backwards compatibility for consumer pipelines.
README.md (What's New in v0.8.5)
--------------------------------
- Reworded lead paragraph to add the thin-YAML port + 35 -> 55
export-count delta.
- New item 8 in the numbered list summarising the thin-YAML port
benefits with a pointer to the full CHANGELOG section.
No code or test changes in this commit - documentation only.
Adds .github/workflows/AzLocal.UpdateManagement-pester.yml - first CI
test workflow in the repository. Runs the AzLocal.UpdateManagement
Pester unit suite on every PR + push to main that touches the module.
Scope
-----
- Path-filtered to AzLocal.UpdateManagement/** + the workflow file
itself, so doc-only or other-module PRs do not burn runner minutes.
- pull_request types: opened, synchronize, reopened (no draft churn).
- workflow_dispatch added so the workflow can be re-run on demand.
- Concurrency group cancels in-progress runs when a new commit lands.
- 20-minute job timeout (full suite is ~5 minutes locally).
Runner
------
- windows-latest is required so the suite exercises the same Windows
PowerShell 5.1 + .NET 4.x surface that the published module targets.
- Pester 5.5.0+ installed at CurrentUser scope if not already present.
- No Az.* modules installed - the unit suite uses mocks for all az
CLI calls (validated locally: 1069 passed, 0 failed, 1 skipped).
Test execution
--------------
- Filter.ExcludeTag = @('Live') keeps the durable Live-Integration
suite out of CI (it needs az login + the AdaptiveCloudLab
subscription and is gated by -IncludeLive in Invoke-Tests.ps1
locally).
- TestResult.OutputFormat = 'JUnitXml' (Pester 5.4+) for compatibility
with dorny/test-reporter's jest-junit reporter.
- Summary written to Tests/TestResults/summary.txt; per-failure lines
to failures.txt; both uploaded as an artifact (14-day retention).
- Job fails when result.FailedCount > 0.
PR check publishing
-------------------
- dorny/test-reporter@v1 publishes the JUnit XML as a checks-API PR
status, surfacing failed test names directly in the PR check tab.
- Only runs on pull_request events. checks: write + pull-requests:
write permissions granted at the job level.
- Known limitation: GITHUB_TOKEN on fork PRs lacks checks: write, so
the reporter step will be skipped (the test job itself still gates
the merge). Not a concern for this single-owner repository.
Why now
-------
Companion to PR #74 (v0.8.5 thin-YAML port). The thin-YAML refactor
moved ~9,000 lines of inline PowerShell out of YAML run: blocks and
into Public cmdlets covered by 1069 Pester tests. Adding CI gating
locks in that investment: future PRs cannot regress the suite without
the failing tests showing up as a red PR check.
Easy to extend later: clone-and-rename this workflow for the
AzLocal.DeploymentAutomation and AzureLocalVM modules once their
suites are equally hermetic.
Three fixes to make the new CI workflow (run 27282472327 = 7 failed,
30 NotRun cascade) go green. None of the failures touched v0.8.5
thin-YAML code - all three are pre-existing test brittleness that
only surfaces on GitHub-hosted Windows runners.
1. Tests: Copy-AzLocalPipelineExample + Copy-AzLocalItsmSample
---------------------------------------------------------
Symptom: 5 of 7 failures. `Should -Be (Join-Path $dest 'subfolder')`
asserted against the function's returned `.FullName` produced:
Expected: 'C:\Users\RUNNER~1\AppData\Local\Temp\azlocal-cpe-...'
Got : 'C:\Users\runneradmin\AppData\Local\Temp\azlocal-cpe-...'
Root cause: $env:TEMP on GitHub-hosted Windows runners returns the
8.3 short-name (`RUNNER~1`) because `runneradmin` exceeds the
8-character limit. Local dev boxes return the long form. The
functions resolve to long form internally; the test fixtures were
keeping the short form.
Fix: resolve $env:TEMP via (Get-Item -LiteralPath $env:TEMP).FullName
inside both BeforeAll blocks before using it to build the test root.
(Get-Item normalises 8.3 short-name segments to long form, where
[IO.Path]::GetFullPath would not.)
No production code touched; assertions unchanged.
2. Workflow: install powershell-yaml on the runner
------------------------------------------------
Symptom: 1 of 7 failures - `ITSM: Get-AzLocalItsmConfig normalises
non-Hashtable YAML dictionaries`. The test mocks ConvertFrom-Yaml,
but Mock requires the target command to exist in the module's
session scope. powershell-yaml isn't preinstalled on
windows-latest runners, so ConvertFrom-Yaml is unresolvable.
Fix: new workflow step `Install powershell-yaml` between Pester
install and the smoke test. Idempotent: skips install if the
module is already available.
3. Workflow: replace dorny/test-reporter with EnricoMi
----------------------------------------------------
Symptom: dorny/test-reporter@v1 emitted
`TypeError: stackTrace.split is not a function`
when parsing Pester's JUnit XML via its jest-junit reporter.
Root cause: Pester serialises stackTrace as a multi-line string
array; jest-junit produces a single string; the reporter assumes
the latter.
Fix: switch to EnricoMi/publish-unit-test-result-action/windows@v2
which understands Pester JUnit shape natively. Comment-mode is
disabled (status is already on the PR via the workflow check).
Also addresses the question raised on the PR thread: 1069/0/1 was
the LOCAL baseline; CI runner is 1039 net tests (excluding 30
that were NotRun due to the cascade from the 5 path failures).
With these three fixes the CI count should match local.
Expected next CI run on PR #74: 1069 passed / 0 failed / 1 skipped.
…e for 7 Export-* cmdlets
CI fix (Publish test report check)
----------------------------------
Pester writes pester-junit.xml via Windows PowerShell 5.1's Set-Content
-Encoding UTF8, which emits a 3-byte UTF-8 BOM. EnricoMi's lxml-based
parser rejects the file with:
ERROR - lxml.etree.XMLSyntaxError: Start tag expected, '<' not found,
line 1, column 1
Error: Error processing result file: missing toplevel element (line 0)
The Pester job itself was green (1069/0/1), but the separate
"AzLocal.UpdateManagement Pester" check (created by EnricoMi when it
fails to parse the report) showed red on PR #74.
Fix: add a workflow step right after the Pester step that re-writes
pester-junit.xml without the BOM, using the BOM-aware UTF-8 reader and
the BOM-less UTF-8 writer. Idempotent (no-op if the file already has
no BOM).
Live-Integration coverage for v0.8.5 thin-YAML cmdlets
------------------------------------------------------
Adds one Describe block to Tests/Live-Integration.Tests.ps1 with one
It per non-destructive Step.N Export-* cmdlet (7 in total):
Step.0 Export-AzLocalAuthValidationReport
Step.3 Export-AzLocalApplyUpdatesScheduleAudit
Step.4 Export-AzLocalFleetConnectivityStatusReport
Step.5 Export-AzLocalClusterUpdateReadinessReport
Step.6 Export-AzLocalClusterReadinessGateReport (empty-ring short-circuit only)
Step.7 Export-AzLocalUpdateRunMonitorReport
Step.8 Export-AzLocalFleetUpdateStatusReport
Step.9 Export-AzLocalFleetHealthStatusReport
Each It runs the cmdlet against a per-test `C:\Users\nebird\AppData\Local\Temp\azlocal-live-exp-<guid>\<slug>-<guid>`
directory with -PassThru, then asserts:
- the documented artifact files exist
- those files are non-empty
- the PassThru payload exposes the documented top-level properties
All artifacts are cleaned up in AfterAll. Read-only: no writes to
Azure. Tagged 'Live'; skipped by default in CI (Invoke-Tests.ps1's
-ExcludeTag 'Live' filter); auto-skipped when `az` isn't logged into
the AdaptiveCloudLab subscription.
Destructive Step.6/Step.6.5 cmdlets (Invoke-AzLocalReadinessGated-
ClusterUpdate, Set-AzLocalClusterUpdateRingTagFromCsv) are explicitly
deferred - they need a scratch-cluster fixture pattern that's tracked
as follow-up.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This PR completes the v0.8.5 thin-YAML port of the
AzLocal.UpdateManagementautomation pipelines. The multi-hundred-line inlinerun: |PowerShell blocks that previously lived inside each Step's GitHub Actions YAML and Azure DevOps YAML are now condensed into Public cmdlets that both pipelines invoke. Zero UI / outcome delta — step output names, artifact contents, JUnit XML, markdown summaries, error messages, exit codes, and stageDependencies bindings are preserved byte-for-byte. Customers running v0.8.4 YAML against the v0.8.5 module continue to work (the install-time drift banner surfaces version skew but does not block).Scope: 19 commits ahead of
main; 45 files changed (12,419 insertions / 8,486 deletions). The total exported function count grows from 35 (v0.8.4) → 55 (v0.8.5).What changed (Step.0 → Step.9)
14 new Public cmdlets, one per Step plus shared helpers. Step.6 (apply-updates — the largest pipeline) splits across 6 cmdlets because of its readiness-gate + ITSM + per-host short-circuit paths.
Add-AzLocalPipelineVersionBanner,Set-AzLocalPipelineOutput,Add-AzLocalPipelineStepSummary,Write-AzLocalPipelineWarning,New-AzLocalPipelineJUnitXmlExport-AzLocalAuthValidationReportInvoke-AzLocalClusterInventorySet-AzLocalClusterUpdateRingTagFromCsvExport-AzLocalApplyUpdatesScheduleAuditExport-AzLocalFleetConnectivityStatusReportExport-AzLocalClusterUpdateReadinessReportResolve-AzLocalPipelineUpdateRing·Export-AzLocalClusterReadinessGateReport·Invoke-AzLocalReadinessGatedClusterUpdate·Add-AzLocalApplyUpdatesStepSummary·Add-AzLocalNoReadyClustersStepSummary·Invoke-AzLocalItsmTicketingFromArtifactExport-AzLocalUpdateRunMonitorReportExport-AzLocalFleetUpdateStatusReportExport-AzLocalFleetHealthStatusReportCumulative reduction across all 20 templates: approximately 9,000 lines removed from
Automation-Pipeline-Examples/.Per-host parity preserved inside the cmdlets
Each new cmdlet auto-detects the host (
$env:TF_BUILDfor ADO,$env:GITHUB_ACTIONSfor GH) and:UPPER_SNAKE(e.g.READY_COUNT); ADO usesPascalCase(e.g.ReadyCount) so existingstageDependencies.<job>.outputs['readiness.ReadyCount']consumers keep working.:white_check_mark:etc.) which render correctly on the Azure DevOps pipeline summary tab.::warning::workflow commands; ADO emits##vso[task.logissue type=warning].The YAML files only carry wiring (triggers, env, OIDC, artifact upload, concurrency, action-version pins) — all of which is preserved exactly.
Benefits (now)
Get-Helpdocumentation. Operators can run any Step's logic interactively (Resolve-AzLocalPipelineUpdateRing -Now ...) instead of copy-pasting from arun:block.Add-AzLocalPipelineVersionBanner,Set-AzLocalPipelineOutput,Add-AzLocalPipelineStepSummary,Write-AzLocalPipelineWarning,New-AzLocalPipelineJUnitXmlare shared across every Step cmdlet. The next pipeline doesn't re-invent host-detection, step-output emission, JUnit framing, or markdown rendering.Benefits (going forward — future maintenance)
Public/<Verb>-AzLocal<Noun>.ps1+ a thinstep:YAML invocation) instead of a copy-paste-from-the-last-Step exercise.Add-AzLocalPipelineVersionBannerat every install step.Pester testing simplification
Pre-port, inline
run: |blocks could only be guarded by string-match Pester assertions against the YAML text (yaml | Should -Match 'foo'). These guards:Post-port, the new Public cmdlets are unit-tested in
Tests/AzLocal.UpdateManagement.Tests.ps1with mockable seams:Get-AzLocalClusterUpdateReadiness,Start-AzLocalClusterUpdate,Resolve-AzLocalCurrentUpdateRing, etc. can be mocked.Itblocks.UPPER_SNAKEvs ADOPascalCase) is asserted by toggling$env:TF_BUILD/$env:GITHUB_ACTIONS.Final Pester baseline on this branch: 1069 passed, 0 failed, 1 skipped, ~4m38s. Function-count drift test bumped 35 → 55 in two sites.
Notable bug fix shaken out during this work
Invoke-AzLocalReadinessGatedClusterUpdate's seven-counter emitter was originally defined as$emitCounters = { ... }.GetNewClosure()..GetNewClosure()rebinds the scriptblock to the caller'sSessionState— which makes Private (non-exported) module functions invisible from inside the closure. The empty-ready short-circuit path then threwSet-AzLocalPipelineOutput is not recognizedbecause the private helper was unreachable. Fix: drop.GetNewClosure()and rely on lexical scoping (the inline scriptblock already has access to the surrounding$n*variables when invoked via& $emitCountersfrom inside the same function). Pester regression guards now cover both GH and ADO short-circuit paths.Documentation
AzLocal.UpdateManagement/CHANGELOG.md— full breakdown under## [0.8.5]→ "Thin-YAML refactor across all 10 Step pipelines (Step.0 - Step.9)".AzLocal.UpdateManagement/README.md— "What's New in v0.8.5" updated with the 35 → 55 export delta and a new item 8 summarising the thin-YAML port.AzLocal.UpdateManagement/Automation-Pipeline-Examples/README.md— no changes needed; it documents file names + appendix links, which are unchanged.Backwards compatibility
name:field, trigger block (cron /workflow_dispatch/ schedules / parameters), env block, concurrency block, OIDC login + Option 2 commented block, action-version pins, step IDs, artifact names, ITSM markers, and per-host step-output naming.Validation
(Import-Module ... -PassThru).ExportedFunctions(55 total).Public/*.ps1glob.*.ps1files are ASCII-compatible (no em-dashes / smart quotes / non-ASCII).