Skip to content

Commit bf67cd9

Browse files
Add generic missed-path coverage reporting (#421)
## Summary Adds missed-path reporting to the upstream Process-PSModule coverage flow so consumer repositories do not need repo-local post-coverage jobs. ## Changes - Extended .github/actions/Get-PesterCodeCoverage to generate a generic missed-path report from merged coverage data. - Added a Missed paths section to the code coverage step summary. - Emit CodeCoverage-MissedPaths.md and CodeCoverage-MissedPaths.json under CodeCoverage-MissedPaths/. - Upload CodeCoverage-MissedPaths as an artifact in .github/workflows/Get-CodeCoverage.yml. ## Notes - The implementation intentionally avoids repo-specific heuristics and groups misses by file path and missed line coverage. - Artifact upload runs with if: always() so the report is still available when coverage threshold enforcement fails. --------- Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent 10fc17b commit bf67cd9

4 files changed

Lines changed: 99 additions & 1 deletion

File tree

.github/actions/Get-PesterCodeCoverage/README.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@ This GitHub Action is a part of the [PSModule framework](https://github.com/PSMo
99
## Features
1010

1111
- Combines multiple code coverage reports from parallel test runs
12-
- Generates markdown/HTML tables showing missed & executed commands
12+
- Generates Markdown/HTML tables showing missed & executed commands
1313
- Displays analyzed files and coverage statistics
14+
- Generates a generic missed-path report (Markdown + JSON)
1415
- Configurable step summary sections
1516
- Threshold enforcement for minimum code coverage
1617

@@ -40,6 +41,10 @@ This GitHub Action is a part of the [PSModule framework](https://github.com/PSMo
4041
4142
## Outputs
4243
44+
| Name | Description |
45+
| ---- | ----------- |
46+
| `MissedPathReportPath` | Folder path containing `CodeCoverage-MissedPaths.md` and `CodeCoverage-MissedPaths.json` |
47+
4348
### GitHub Step Summary
4449

4550
The action generates a detailed summary visible in the GitHub Actions UI:
@@ -53,6 +58,7 @@ The action generates a detailed summary visible in the GitHub Actions UI:
5358
- **Missed Commands**: HTML table with code snippets
5459
- **Executed Commands**: HTML table with code snippets
5560
- **Analyzed Files**: List of covered files
61+
- **Missed Paths**: Aggregated missed-command counts grouped by file path
5662

5763
Example summary:
5864

.github/actions/Get-PesterCodeCoverage/action.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,11 @@ inputs:
3535
description: The target for code coverage.
3636
required: false
3737

38+
outputs:
39+
MissedPathReportPath:
40+
description: Path to the generated missed-path report folder.
41+
value: ${{ steps.Get-PesterCodeCoverage.outputs.MissedPathReportPath }}
42+
3843
runs:
3944
using: composite
4045
steps:

.github/actions/Get-PesterCodeCoverage/src/main.ps1

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,73 @@ $success = $coveragePercent -ge $coveragePercentTarget
158158
$statusIcon = $success ? '' : ''
159159
$stats | Format-Table -AutoSize | Out-String
160160

161+
$missedPathGroups = $codeCoverage.CommandsMissed |
162+
Where-Object { -not [string]::IsNullOrWhiteSpace($_.File) } |
163+
Group-Object -Property File |
164+
Sort-Object -Property @(
165+
@{ Expression = 'Count'; Descending = $true },
166+
@{ Expression = 'Name'; Descending = $false }
167+
)
168+
169+
$missedPaths = foreach ($group in $missedPathGroups) {
170+
$lines = $group.Group |
171+
Where-Object { $_.Line -is [ValueType] } |
172+
ForEach-Object { [int]$_.Line } |
173+
Sort-Object -Unique
174+
$missedLines = if ($lines.Count -eq 0) { '' } else { $lines -join ', ' }
175+
[pscustomobject]@{
176+
Path = $group.Name
177+
MissedCommands = [int]$group.Count
178+
MissedLines = $missedLines
179+
}
180+
}
181+
182+
$missedPathReportPath = 'CodeCoverage-MissedPaths'
183+
$null = New-Item -Path $missedPathReportPath -ItemType Directory -Force
184+
185+
$missedPathReport = [ordered]@{
186+
GeneratedAtUtc = [DateTime]::UtcNow.ToString('o')
187+
CoveragePercent = [double]$coveragePercent
188+
CoverageTarget = [double]$coveragePercentTarget
189+
MissedPaths = @($missedPaths)
190+
}
191+
192+
$missedPathReportJsonPath = Join-Path -Path $missedPathReportPath -ChildPath 'CodeCoverage-MissedPaths.json'
193+
$missedPathReportMarkdownPath = Join-Path -Path $missedPathReportPath -ChildPath 'CodeCoverage-MissedPaths.md'
194+
$missedPathReport | ConvertTo-Json -Depth 10 | Set-Content -Path $missedPathReportJsonPath -Encoding utf8NoBOM
195+
196+
$missedPathMarkdown = [System.Collections.Generic.List[string]]::new()
197+
$null = $missedPathMarkdown.Add('# Code Coverage Missed Paths')
198+
$null = $missedPathMarkdown.Add('')
199+
$null = $missedPathMarkdown.Add("| Coverage | Target | Missed Paths | Missed Commands |")
200+
$null = $missedPathMarkdown.Add("| --- | --- | --- | --- |")
201+
$coverageSummaryRow = '| {0}% | {1}% | {2} | {3} |' -f (
202+
[Math]::Round($coveragePercent, 2)
203+
), (
204+
[Math]::Round($coveragePercentTarget, 2)
205+
), $missedPaths.Count, $codeCoverage.CommandsMissedCount
206+
$null = $missedPathMarkdown.Add($coverageSummaryRow)
207+
$null = $missedPathMarkdown.Add('')
208+
$null = $missedPathMarkdown.Add('## Paths')
209+
$null = $missedPathMarkdown.Add('| Path | Missed Commands | Missed Lines |')
210+
$null = $missedPathMarkdown.Add('| --- | ---: | --- |')
211+
if ($missedPaths.Count -eq 0) {
212+
$null = $missedPathMarkdown.Add('| _None_ | 0 | |')
213+
} else {
214+
foreach ($pathEntry in $missedPaths) {
215+
$safePath = ([string]$pathEntry.Path).Replace('|', '\|')
216+
$safeLines = ([string]$pathEntry.MissedLines).Replace('|', '\|')
217+
$null = $missedPathMarkdown.Add("| $safePath | $($pathEntry.MissedCommands) | $safeLines |")
218+
}
219+
}
220+
221+
$missedPathMarkdown | Set-Content -Path $missedPathReportMarkdownPath -Encoding utf8NoBOM
222+
Write-Output 'Missed path report generated:'
223+
$missedPaths | Select-Object -First 20 | Format-Table -Property Path, MissedCommands, MissedLines -AutoSize | Out-String
224+
if ($env:GITHUB_OUTPUT) {
225+
"MissedPathReportPath=$missedPathReportPath" >> $env:GITHUB_OUTPUT
226+
}
227+
161228
# Build HTML table for 'missed' commands
162229
$tableheader = @'
163230
<table>
@@ -300,6 +367,18 @@ LogGroup 'Step Summary - Set step summary' {
300367
}
301368
}
302369
}
370+
371+
Details "Missed paths [$($missedPaths.Count)]" {
372+
if ($missedPaths.Count -eq 0) {
373+
Paragraph {
374+
Write-Output 'No missed paths were detected.'
375+
}
376+
} else {
377+
Table {
378+
$missedPaths | Select-Object -First 100
379+
}
380+
}
381+
}
303382
}
304383
}
305384

.github/workflows/Get-CodeCoverage.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,11 @@ jobs:
3434
Prerelease: ${{ fromJson(inputs.Settings).Prerelease }}
3535
Verbose: ${{ fromJson(inputs.Settings).Verbose }}
3636
Version: ${{ fromJson(inputs.Settings).Version }}
37+
38+
- name: Upload CodeCoverage-MissedPaths artifact
39+
if: always()
40+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
41+
with:
42+
name: CodeCoverage-MissedPaths
43+
path: CodeCoverage-MissedPaths
44+
if-no-files-found: warn

0 commit comments

Comments
 (0)