Commit 629c501
authored
AzLocal.UpdateManagement v0.8.5: thin-YAML port across all 10 Step pipelines (35 → 55 cmdlets, zero UI delta) (#74)
* Publish v0.8.4
* v0.8.4 follow-up: correct ADO Step.6 banner count + allow migration-tip 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.
* Add CRON for Step 6 - Apply Updates
* v0.8.5: thin-YAML foundation cmdlet Add-AzLocalPipelineVersionBanner
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.
* v0.8.5: Step.6 manual schedule-file inputs (use_schedule_file + resolve_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.
* v0.8.5 docs: CHANGELOG + README What's New entry
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.
* v0.8.5 Step.0 thin-YAML port: Export-AzLocalAuthValidationReport + shared 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).
* v0.8.5 Step.1 thin-YAML port: Add Invoke-AzLocalClusterInventory
- 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.
* v0.8.5 Step.2 thin-YAML port: Add Set-AzLocalClusterUpdateRingTagFromCsv
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.
* v0.8.5 Step.7 thin-YAML port: Add Export-AzLocalUpdateRunMonitorReport
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.
* v0.8.5 Step.8 thin-YAML: Export-AzLocalFleetUpdateStatusReport
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.
* v0.8.5 Step.5 thin-YAML: Export-AzLocalClusterUpdateReadinessReport
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.
* v0.8.5 Step.4 thin-YAML: Export-AzLocalFleetConnectivityStatusReport
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.
* v0.8.5 Step.3 thin-YAML: Export-AzLocalApplyUpdatesScheduleAudit
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).
* v0.8.5 Step.7 test: lock in target=_blank on In-flight runs links
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.
* v0.8.5 Step.9 thin-YAML: Export-AzLocalFleetHealthStatusReport
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
* v0.8.5 Step.6 thin-YAML: apply-updates pipeline refactor
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).
* v0.8.5 docs: CHANGELOG + README updated for thin-YAML refactor
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.
* ci: add GitHub Actions Pester workflow for AzLocal.UpdateManagement
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.
* ci: fix Windows-runner test failures + switch test reporter
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.
* ci+tests: fix EnricoMi BOM parse error + add Live-Integration coverage 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.1 parent e55b765 commit 629c501
47 files changed
Lines changed: 12735 additions & 8488 deletions
File tree
- .github/workflows
- AzLocal.UpdateManagement
- Automation-Pipeline-Examples
- azure-devops
- github-actions
- Private
- Public
- Tests
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
0 commit comments