Skip to content

Commit 629c501

Browse files
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

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
name: AzLocal.UpdateManagement - Pester
2+
3+
# Runs the AzLocal.UpdateManagement Pester unit suite on every PR + push to main
4+
# that touches the module. Live-tagged Azure integration tests are excluded
5+
# (they need az login + the AdaptiveCloudLab subscription and are gated by
6+
# -IncludeLive in the local Invoke-Tests.ps1).
7+
#
8+
# Runner choice: windows-latest is required so the tests exercise the same
9+
# Windows PowerShell 5.1 + .NET 4.x surface that the published module targets.
10+
# Cost mitigation: path-filtered to AzLocal.UpdateManagement/** only, and only
11+
# fires on the three useful PR activities (opened, synchronize, reopened).
12+
13+
on:
14+
pull_request:
15+
types: [opened, synchronize, reopened]
16+
paths:
17+
- 'AzLocal.UpdateManagement/**'
18+
- '.github/workflows/AzLocal.UpdateManagement-pester.yml'
19+
push:
20+
branches: [main]
21+
paths:
22+
- 'AzLocal.UpdateManagement/**'
23+
- '.github/workflows/AzLocal.UpdateManagement-pester.yml'
24+
workflow_dispatch:
25+
26+
concurrency:
27+
group: azlocal-updatemanagement-pester-${{ github.ref }}
28+
cancel-in-progress: true
29+
30+
permissions:
31+
contents: read
32+
checks: write
33+
pull-requests: write
34+
35+
jobs:
36+
pester:
37+
name: Pester (Windows PowerShell 5.1)
38+
runs-on: windows-latest
39+
timeout-minutes: 20
40+
41+
defaults:
42+
run:
43+
shell: powershell
44+
working-directory: AzLocal.UpdateManagement
45+
46+
steps:
47+
- name: Checkout
48+
uses: actions/checkout@v4
49+
50+
- name: Show PowerShell + Pester baseline
51+
run: |
52+
$PSVersionTable | Format-List | Out-String | Write-Host
53+
Get-Module Pester -ListAvailable | Select-Object Name, Version, Path | Format-Table -AutoSize | Out-String | Write-Host
54+
55+
- name: Install Pester 5.x
56+
run: |
57+
$needed = $true
58+
$installed = Get-Module Pester -ListAvailable | Where-Object { $_.Version -ge [version]'5.0.0' } | Sort-Object Version -Descending | Select-Object -First 1
59+
if ($installed) {
60+
Write-Host "Pester $($installed.Version) already available at $($installed.Path)"
61+
$needed = $false
62+
}
63+
if ($needed) {
64+
Write-Host 'Installing Pester 5.x from PSGallery (CurrentUser scope)'
65+
Install-Module Pester -MinimumVersion 5.5.0 -Force -SkipPublisherCheck -Scope CurrentUser
66+
}
67+
Import-Module Pester -MinimumVersion 5.0.0 -Force
68+
(Get-Module Pester).Version | Write-Host
69+
70+
- name: Install powershell-yaml
71+
# Required by one Describe block ('ITSM: Get-AzLocalItsmConfig normalises
72+
# non-Hashtable YAML dictionaries') which Mock-overrides ConvertFrom-Yaml.
73+
# Mock can only intercept commands that exist, so the source module must
74+
# be present even though no actual YAML parsing happens at test time.
75+
run: |
76+
if (-not (Get-Module powershell-yaml -ListAvailable)) {
77+
Write-Host 'Installing powershell-yaml from PSGallery (CurrentUser scope)'
78+
Install-Module powershell-yaml -Force -SkipPublisherCheck -Scope CurrentUser
79+
} else {
80+
Write-Host 'powershell-yaml already available'
81+
}
82+
(Get-Module powershell-yaml -ListAvailable | Select-Object -First 1).Version | Write-Host
83+
84+
- name: Import module (smoke test)
85+
run: |
86+
Get-Module AzLocal.UpdateManagement -All | Remove-Module -Force -ErrorAction SilentlyContinue
87+
Import-Module .\AzLocal.UpdateManagement.psd1 -Force -ErrorAction Stop
88+
$m = Get-Module AzLocal.UpdateManagement
89+
Write-Host "Loaded module $($m.Name) $($m.Version) with $($m.ExportedFunctions.Count) exported functions"
90+
91+
- name: Run Pester (unit suite, Live excluded)
92+
run: |
93+
New-Item -ItemType Directory -Path Tests\TestResults -Force | Out-Null
94+
Get-Module AzLocal.UpdateManagement -All | Remove-Module -Force -ErrorAction SilentlyContinue
95+
$config = New-PesterConfiguration
96+
$config.Run.Path = '.\Tests'
97+
$config.Run.PassThru = $true
98+
$config.Run.Exit = $false
99+
$config.Filter.ExcludeTag = @('Live')
100+
$config.TestResult.Enabled = $true
101+
$config.TestResult.OutputPath = 'Tests\TestResults\pester-junit.xml'
102+
# JUnitXml is supported by Pester 5.4+ and is consumable by
103+
# dorny/test-reporter with reporter=jest-junit below.
104+
$config.TestResult.OutputFormat = 'JUnitXml'
105+
$config.Output.Verbosity = 'Normal'
106+
$result = Invoke-Pester -Configuration $config
107+
"Passed=$($result.PassedCount) Failed=$($result.FailedCount) Skipped=$($result.SkippedCount) Duration=$($result.Duration)" | Tee-Object -FilePath Tests\TestResults\summary.txt
108+
if ($result.FailedCount -gt 0) {
109+
$result.Failed | ForEach-Object { "FAIL: $($_.ExpandedPath) :: $($_.ErrorRecord.Exception.Message)" } | Out-File Tests\TestResults\failures.txt
110+
Write-Error "Pester reported $($result.FailedCount) failing test(s)."
111+
exit 1
112+
}
113+
114+
- name: Strip UTF-8 BOM from JUnit XML
115+
# Windows PowerShell 5.1's UTF-8 writer (which Pester uses for the
116+
# JUnit output) emits a 3-byte BOM. The EnricoMi publish-test-results
117+
# action's lxml parser rejects it with: "Start tag expected, '<' not
118+
# found, line 1, column 1" / "missing toplevel element". Re-write the
119+
# file without BOM (read with BOM-aware UTF-8, write with BOM-less
120+
# UTF-8). Idempotent: if the file is already BOM-less this is a no-op.
121+
if: always()
122+
run: |
123+
$xml = 'Tests\TestResults\pester-junit.xml'
124+
if (Test-Path -LiteralPath $xml) {
125+
$bytes = [System.IO.File]::ReadAllBytes($xml)
126+
$hasBom = ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF)
127+
if ($hasBom) {
128+
$text = [System.IO.File]::ReadAllText($xml, [System.Text.UTF8Encoding]::new($true))
129+
[System.IO.File]::WriteAllText($xml, $text, [System.Text.UTF8Encoding]::new($false))
130+
Write-Host "Stripped UTF-8 BOM from $xml (was $($bytes.Length) bytes, now $((Get-Item -LiteralPath $xml).Length) bytes)"
131+
} else {
132+
Write-Host "$xml has no BOM; nothing to strip."
133+
}
134+
} else {
135+
Write-Host "$xml not found; skipping BOM strip."
136+
}
137+
138+
- name: Upload test results
139+
if: always()
140+
uses: actions/upload-artifact@v4
141+
with:
142+
name: pester-results-${{ github.run_id }}
143+
path: AzLocal.UpdateManagement/Tests/TestResults/
144+
if-no-files-found: warn
145+
retention-days: 14
146+
147+
- name: Publish test report (PR check)
148+
# EnricoMi/publish-unit-test-result-action understands Pester's
149+
# JUnitXml output natively. dorny/test-reporter was rejected because
150+
# its 'jest-junit' parser threw `stackTrace.split is not a function`
151+
# on Pester's stackTrace shape (Pester serialises it as a multi-line
152+
# string array, not the single-string shape jest produces).
153+
if: always() && github.event_name == 'pull_request'
154+
uses: EnricoMi/publish-unit-test-result-action/windows@v2
155+
with:
156+
check_name: AzLocal.UpdateManagement Pester
157+
junit_files: AzLocal.UpdateManagement/Tests/TestResults/pester-junit.xml
158+
comment_mode: off
159+
ignore_runs: true

0 commit comments

Comments
 (0)