Modules/businessdev.ALbuild.RuntimePackages/Private/Get-BcRuntimeAppDiagnostic.ps1

function Get-BcRuntimeAppDiagnostic {
    <#
    .SYNOPSIS
        Picks the compiler diagnostics that belong to ONE app out of a worker log.
 
    .DESCRIPTION
        A worker log holds every app of one platform version, one after the other. Grepping it for
        'error AL....' and printing the hits under whichever app just failed is how a log starts lying:
        run 27378 reported 'logo.png is being used by another process' as a failure of Sanction Screen
        while the path in the message named the API.
 
        The log has honest boundaries - the compile of each app is announced with "Compiling '<name>'"
        and closed with 'Compilation ended' - so the diagnostics are read between them, and only the LAST
        such block is used: a chain app may be compiled several times in one container, and the failure
        is in the attempt that just happened.
 
    .PARAMETER LogLine
        The worker log for one platform version. Optional, and empty content is allowed: a Mandatory
        [string[]] rejects a single empty element with "cannot bind argument ... because it is an empty
        string", and a worker log whose only line is blank is a perfectly ordinary thing. In run 27404
        that binding error came out of the checkpoint and took both Legacy slices with it.
 
    .PARAMETER AppName
        The app whose diagnostics are wanted.
 
    .PARAMETER Max
        Upper bound; the rest are summarised as a count.
 
    .OUTPUTS
        System.String[] - the diagnostic lines, already trimmed of their absolute build path.
    #>

    [CmdletBinding()]
    [OutputType([string[]])]
    param(
        [AllowNull()] [AllowEmptyCollection()] [AllowEmptyString()] [string[]] $LogLine = @(),
        [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $AppName,
        [ValidateRange(1, 50)] [int] $Max = 5
    )

    # Two statements, not an 'if' expression: an if that yields a ONE-element array collapses to the
    # element on assignment, and on Windows PowerShell 5.1 a scalar has no .Count at all - under
    # Set-StrictMode that is a terminating error. Build 27406's dual-target leg caught both halves of
    # this; a PS7-only run sees neither, because there every object has .Count.
    $lines = @()
    if ($null -ne $LogLine) { $lines = @($LogLine) }
    if ($lines.Count -eq 0) { return @() }

    $start = -1
    for ($i = 0; $i -lt $lines.Count; $i++) {
        if ($lines[$i] -match "Compiling '$([regex]::Escape($AppName))'") { $start = $i }
    }
    if ($start -lt 0) { return @() }

    $found = [System.Collections.Generic.List[string]]::new()
    for ($i = $start; $i -lt $lines.Count; $i++) {
        if ($i -gt $start -and $lines[$i] -match 'Compilation ended') { break }
        if ($lines[$i] -notmatch '\berror [A-Za-z]{2}\d+:') { continue }
        # Drop the build path: it is a temp folder with a guid in it, the same on every line, and it
        # pushes the part that matters off the right edge.
        $found.Add(($lines[$i] -replace '^.*[\\/](?=[^\\/]+\.(al|json)\()', '' -replace '^\s+', ''))
    }

    if ($found.Count -le $Max) { return $found.ToArray() }
    $head = @($found | Select-Object -First $Max)
    return @($head + @("... and $($found.Count - $Max) more diagnostic(s) - the full log is in the 'worker-logs' artifact"))
}