Public/Assert-AzLocalPipelineReport.ps1

function Assert-AzLocalPipelineReport {
    <#
    .SYNOPSIS
        Fails the pipeline step with a meaningful run-summary message when the
        expected diagnostic report file(s) were not produced.
 
    .DESCRIPTION
        The AzLocal.UpdateManagement pipelines write their JUnit / CSV / JSON
        reports to a working directory (e.g. ./reports or ./artifacts) which a
        later `dorny/test-reporter` (GitHub) or `PublishTestResults` (Azure
        DevOps) step then publishes. When the collect step fails or produces no
        output (most commonly because of an upstream Azure login / subscription
        failure), the publish step emits the misleading error
        "No test report files were found" - a symptom, not the cause.
 
        This cmdlet is the meaningful gate placed BETWEEN the collect step and
        the publish step:
          1. Expands each -Path glob and gathers the matching files.
          2. When -RequireNonEmpty (the default) is set, ignores zero-byte files.
          3. If no qualifying file exists:
               - emits a red `::error` / `##vso[task.logissue type=error]`
                 annotation via Write-AzLocalPipelineError,
               - writes a rich explanation to the run summary via
                 Add-AzLocalPipelineStepSummary (so the operator does NOT have
                 to expand the step logs),
               - throws a concise terminating error so the step exits non-zero
                 BEFORE the confusing publish-step error can fire.
          4. Otherwise it writes a one-line confirmation to the summary and
             (with -PassThru) returns the matching file paths.
 
    .PARAMETER Path
        One or more file paths or globs that the collect step is expected to
        have produced (e.g. './reports/*.xml'). At least one qualifying file
        across all paths must exist for the step to pass.
 
    .PARAMETER ProducingStepName
        Friendly name of the step that was supposed to produce the reports,
        used in the summary text (e.g. 'Collect Fleet Health Status'). Defaults
        to 'the collect step'.
 
    .PARAMETER AllowEmpty
        By default a matching file must be larger than zero bytes to count. Set
        -AllowEmpty to accept zero-byte files as well.
 
    .PARAMETER SummaryFileName
        Forwarded to Add-AzLocalPipelineStepSummary. Defaults to
        'azlocal-report-check.md'. Ignored on GitHub Actions.
 
    .PARAMETER PassThru
        When set, returns the array of matching file paths on success.
 
    .OUTPUTS
        Nothing by default. With -PassThru, returns [string[]] of matching file
        paths. Throws a terminating error when no qualifying file exists.
 
    .EXAMPLE
        Assert-AzLocalPipelineReport -Path './reports/*.xml' -ProducingStepName 'Collect Fleet Health Status'
 
    .EXAMPLE
        $files = Assert-AzLocalPipelineReport -Path './artifacts/assess-readiness.xml' -PassThru
    #>

    [CmdletBinding()]
    [OutputType([string[]])]
    param(
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string[]]$Path,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]$ProducingStepName = 'the collect step',

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]$SummaryFileName = 'azlocal-report-check.md',

        # When present, zero-byte matches also count as valid reports. Off by
        # default so an empty (0-byte) report file is treated as "no report".
        [Parameter()]
        [switch]$AllowEmpty,

        [Parameter()]
        [switch]$PassThru
    )

    # 1. Expand every glob and collect matching FILES.
    $matched = [System.Collections.Generic.List[string]]::new()
    foreach ($p in $Path) {
        $items = Get-ChildItem -Path $p -File -ErrorAction SilentlyContinue
        foreach ($item in $items) {
            if (-not $AllowEmpty -and $item.Length -le 0) { continue }
            $matched.Add($item.FullName)
        }
    }
    $matchedFiles = $matched.ToArray()

    # 2. Failure path: nothing produced -> meaningful error + run-summary block + throw.
    if ($matchedFiles.Count -eq 0) {
        $pathList = ($Path | ForEach-Object { "``$_``" }) -join ', '
        $emptyNote = if ($AllowEmpty) { '' } else { ' (non-empty)' }

        $summary = @"
## :x: No diagnostic reports were produced
 
$ProducingStepName produced no$emptyNote report file(s) matching: $pathList
 
The publish step that follows would normally report "No test report files were
found" - but that is a **symptom**, not the cause. The collect step above did
not write any output, which almost always means an **upstream failure** (most
commonly the Azure login / subscription-access step). Check the earlier steps in
this job for the first red step and fix that; the reports and their published
results will follow.
"@


        Write-AzLocalPipelineError `
            -Title 'No diagnostic reports were produced' `
            -Message ("$ProducingStepName produced no$emptyNote report files matching $pathList. " +
                'This is caused by an upstream failure (commonly Azure login / subscription access), not the publish step. See the run summary and check the first failing step above.')

        [void](Add-AzLocalPipelineStepSummary -Markdown $summary -SummaryFileName $SummaryFileName)

        throw "Assert-AzLocalPipelineReport: no report files produced by '$ProducingStepName' (expected: $($Path -join ', ')). See the run summary for details."
    }

    # 3. Success path.
    Write-Host "Diagnostic reports verified: $($matchedFiles.Count) file(s) produced by '$ProducingStepName'."
    $okSummary = "_Diagnostic reports verified: **$($matchedFiles.Count)** file(s) produced by ${ProducingStepName}._`n"
    [void](Add-AzLocalPipelineStepSummary -Markdown $okSummary -SummaryFileName $SummaryFileName)

    if ($PassThru) { return $matchedFiles }
}