Private/Output/Get-AvmFailureDetail.ps1
|
function Get-AvmSeverityWeight { <# .SYNOPSIS Order a diagnostic severity so an error outranks a warning, which outranks an informational note. .DESCRIPTION Engines emit severities in their own casing and vocabulary - tflint reports 'notice', which the renderer shows as 'info' - so the comparison is case-insensitive and scores 'notice' and 'info' as the same tier. An unrecognised or absent severity scores between error and info rather than at either end, so an engine that introduces a new name can neither silently outrank a real error nor lose to a nit. #> [CmdletBinding()] [OutputType([int])] param( [AllowNull()] [object] $Severity ) Set-StrictMode -Version 3.0 $text = ([string]$Severity).Trim() if ([string]::IsNullOrWhiteSpace($text)) { return 2 } switch ($text.ToLowerInvariant()) { 'error' { return 3 } 'warning' { return 2 } 'warn' { return 2 } 'info' { return 1 } 'information' { return 1 } 'notice' { return 1 } default { return 2 } } } function Get-AvmIssueSeverityWeight { [CmdletBinding()] [OutputType([int])] param( [AllowNull()] [object] $Issue ) Set-StrictMode -Version 3.0 if ($null -eq $Issue) { return 2 } $severityProperty = $Issue.PSObject.Properties['Severity'] if ($null -eq $severityProperty) { return 2 } return (Get-AvmSeverityWeight -Severity $severityProperty.Value) } function Get-AvmPrimaryIssue { <# .SYNOPSIS Pick the single most actionable issue on a result. .DESCRIPTION A failing result often carries a bare status issue (for example terraform's "test run 'apply' fail", which has no line number) ahead of the diagnostic that names the file, position and cause. The headline a user sees - the console summary, the GitHub Actions annotation and the terminating error message - must be the one that points at the problem, so an issue carrying a line number wins over one that does not. Every issue here describes the same step failing, so position dominates: the bare status issue is a restatement of the diagnostic beneath it, not a separate problem, and it must lose even when it is scored the more severe of the two. Severity only breaks ties between equally positioned issues - tflint reports an 'info' nit and the 'warning' that actually failed the step both with a line, and naming the nit as the reason the step failed states a cause that is not the cause. #> [CmdletBinding()] param( [Parameter(Mandatory)] [object] $Result ) Set-StrictMode -Version 3.0 $issuesProperty = $Result.PSObject.Properties['Issues'] if ($null -eq $issuesProperty) { return $null } $issues = @(@($issuesProperty.Value) | Where-Object { $null -ne $_ }) if ($issues.Count -eq 0) { return $null } $best = $null $bestScore = -1 foreach ($issue in $issues) { $lineProperty = $issue.PSObject.Properties['Line'] $positioned = $null -ne $lineProperty -and [int]$lineProperty.Value -gt 0 # Position outranks severity outright, so the weights cannot overlap. $score = (Get-AvmIssueSeverityWeight -Issue $issue) if ($positioned) { $score += 10 } if ($score -gt $bestScore) { $best = $issue $bestScore = $score } } return $best } function Get-AvmIssuePosition { [CmdletBinding()] [OutputType([hashtable])] param( [AllowNull()] [object] $Issue ) Set-StrictMode -Version 3.0 if ($null -eq $Issue) { return $null } $fileProperty = $Issue.PSObject.Properties['File'] if ($null -eq $fileProperty -or [string]::IsNullOrWhiteSpace([string]$fileProperty.Value)) { return $null } $lineProperty = $Issue.PSObject.Properties['Line'] $columnProperty = $Issue.PSObject.Properties['Column'] return @{ File = [string]$fileProperty.Value Line = if ($null -ne $lineProperty) { [int]$lineProperty.Value } else { 0 } Column = if ($null -ne $columnProperty) { [int]$columnProperty.Value } else { 0 } } } function Get-AvmPrimaryIssueLine { [CmdletBinding()] [OutputType([string])] param( [Parameter(Mandatory)] [object] $Result ) Set-StrictMode -Version 3.0 $chosen = Get-AvmPrimaryIssue -Result $Result if ($null -eq $chosen) { return '' } $lines = @(ConvertTo-AvmIssueLine -Result ([pscustomobject]@{ Issues = @($chosen) })) if ($lines.Count -gt 0) { return $lines[0] } return '' } function Get-AvmStepPosition { <# .SYNOPSIS Locate the position carried by a single failing step's headline diagnostic, or null when it has none. #> [CmdletBinding()] [OutputType([hashtable])] param( [AllowNull()] [object] $Step ) Set-StrictMode -Version 3.0 if ($null -eq $Step) { return $null } $errorProperty = $Step.PSObject.Properties['Error'] if ($null -ne $errorProperty -and -not [string]::IsNullOrWhiteSpace([string]$errorProperty.Value)) { return $null } $resultProperty = $Step.PSObject.Properties['Result'] if ($null -ne $resultProperty -and $null -ne $resultProperty.Value) { return (Get-AvmIssuePosition -Issue (Get-AvmPrimaryIssue -Result $resultProperty.Value)) } return $null } function Get-AvmStepSeverityWeight { <# .SYNOPSIS Score how severe a failing step's headline problem is. .DESCRIPTION A step whose tool broke outright carries an Error string and no issues, and is scored as an error: the check did not run, which is at least as serious as one that ran and reported. Otherwise the score comes from the headline diagnostic, so a step is ranked by the same issue whose text is printed for it. #> [CmdletBinding()] [OutputType([int])] param( [AllowNull()] [object] $Step ) Set-StrictMode -Version 3.0 if ($null -eq $Step) { return 2 } $errorProperty = $Step.PSObject.Properties['Error'] if ($null -ne $errorProperty -and -not [string]::IsNullOrWhiteSpace([string]$errorProperty.Value)) { return (Get-AvmSeverityWeight -Severity 'error') } $resultProperty = $Step.PSObject.Properties['Result'] if ($null -ne $resultProperty -and $null -ne $resultProperty.Value) { $issue = Get-AvmPrimaryIssue -Result $resultProperty.Value if ($null -ne $issue) { return (Get-AvmIssueSeverityWeight -Issue $issue) } } $statusProperty = $Step.PSObject.Properties['Status'] if ($null -ne $statusProperty -and [string]$statusProperty.Value -eq 'error') { return (Get-AvmSeverityWeight -Severity 'error') } return 2 } function Get-AvmStepRank { <# .SYNOPSIS Score a failing step by how serious its headline diagnostic is, then by how precisely it points at the problem. .DESCRIPTION Steps in one run are separate problems, so severity decides between them first: a run carrying formatting drift, a lint nit and a broken test must headline the test, not the nit, even though both name a line. This is the opposite weighting to Get-AvmPrimaryIssue, which chooses between issues describing a single failure and so must let position dominate. Precision breaks ties within a severity. A file on its own is not the same as a file and a line - terraform's formatting drift names the file it would rewrite but has no position inside it, so an annotation built from it lands on the file header rather than on any code. The line test matches Get-AvmPrimaryIssue's, so a step is ranked by the same measure used to choose between the issues within it. #> [CmdletBinding()] [OutputType([int])] param( [AllowNull()] [object] $Step ) Set-StrictMode -Version 3.0 $precision = 0 $position = Get-AvmStepPosition -Step $Step if ($null -ne $position) { $precision = if ([int]$position.Line -gt 0) { 2 } else { 1 } } # Severity outranks precision outright, so the weights cannot overlap. return ((Get-AvmStepSeverityWeight -Step $Step) * 10) + $precision } function Get-AvmFailureStep { <# .SYNOPSIS Pick the failing step whose diagnostic should headline the run. .DESCRIPTION F58: a chain reports every failing step rather than stopping at the first, so pipeline order says nothing about which failure is the most actionable. One stray space fails format, transform, lint and docs at once, and taking the first of those headlines formatting drift while a broken test later in the same run - carrying the only file, line and column in it - never reaches the single GitHub Actions annotation. The developer then sees a nit in the Files-changed view and nothing about the test. The step whose headline diagnostic is the most serious therefore wins, with precision breaking ties within a severity. Order still decides between steps that tie on both, so a run whose failures are equally serious and equally precise reports exactly as it did before. #> [CmdletBinding()] param( [Parameter(Mandatory)] [object] $Result ) Set-StrictMode -Version 3.0 $stepsProperty = $Result.PSObject.Properties['Steps'] if ($null -eq $stepsProperty) { return $null } $failing = @(@($stepsProperty.Value) | Where-Object { $null -ne $_ -and [string]$_.Status -in @('fail', 'error') }) if ($failing.Count -eq 0) { return $null } $best = $failing[0] $bestRank = Get-AvmStepRank -Step $best foreach ($step in $failing) { $rank = Get-AvmStepRank -Step $step if ($rank -gt $bestRank) { $best = $step $bestRank = $rank } } return $best } function Get-AvmFailureDetail { [CmdletBinding()] [OutputType([string])] param( [Parameter(Mandatory)] [object] $Result ) Set-StrictMode -Version 3.0 $step = Get-AvmFailureStep -Result $Result if ($null -ne $step) { $errorProperty = $step.PSObject.Properties['Error'] if ($null -ne $errorProperty -and -not [string]::IsNullOrWhiteSpace([string]$errorProperty.Value)) { return "Step '$($step.Step)': $($errorProperty.Value)" } $resultProperty = $step.PSObject.Properties['Result'] if ($null -ne $resultProperty -and $null -ne $resultProperty.Value) { $issueLine = Get-AvmPrimaryIssueLine -Result $resultProperty.Value if (-not [string]::IsNullOrWhiteSpace($issueLine)) { return "Step '$($step.Step)': $issueLine" } } return "Step '$($step.Step)' reported Status '$($step.Status)'." } return (Get-AvmPrimaryIssueLine -Result $Result) } function Get-AvmFailurePosition { <# .SYNOPSIS Locate the file, line and column behind the detail Get-AvmFailureDetail returns, so the GitHub Actions annotation can be anchored on it. .DESCRIPTION Both walks start from the same Get-AvmFailureStep choice, so the annotation is anchored on the step whose text is being reported and a branch that yields a positionless detail still yields a null position. #> [CmdletBinding()] [OutputType([hashtable])] param( [Parameter(Mandatory)] [object] $Result ) Set-StrictMode -Version 3.0 $step = Get-AvmFailureStep -Result $Result if ($null -ne $step) { return (Get-AvmStepPosition -Step $step) } return (Get-AvmIssuePosition -Issue (Get-AvmPrimaryIssue -Result $Result)) } |