Add code coverage comment on the PR #1
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Code Coverage Comparison | |
| on: | |
| workflow_dispatch: # Allows manual triggering from Actions tab | |
| pull_request: | |
| branches: | |
| - main | |
| paths: | |
| - 'src/**' | |
| - 'test/**' | |
| - 'build/**/*.yml' | |
| - '.github/workflows/**' | |
| - 'CodeCoverage.runsettings' | |
| permissions: | |
| contents: read | |
| pull-requests: write | |
| actions: read | |
| jobs: | |
| coverage-comparison: | |
| name: Compare Code Coverage | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout PR branch | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Setup .NET | |
| uses: actions/setup-dotnet@v4 | |
| with: | |
| dotnet-version: '9.0.x' | |
| - name: Install coverage tools | |
| run: | | |
| dotnet tool install --global dotnet-coverage | |
| dotnet tool install --global dotnet-reportgenerator-globaltool | |
| - name: Build and test PR branch with coverage | |
| run: | | |
| dotnet build --configuration Release | |
| dotnet test \ | |
| --configuration Release \ | |
| --no-build \ | |
| --collect "XPlat Code Coverage" \ | |
| --settings CodeCoverage.runsettings \ | |
| --results-directory ./TestResults/PR \ | |
| -- DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.Format=cobertura | |
| - name: Merge PR coverage reports | |
| run: | | |
| dotnet-coverage merge ./TestResults/PR/**/coverage.cobertura.xml \ | |
| --output ./TestResults/pr-coverage.xml \ | |
| --output-format cobertura | |
| - name: Checkout main branch | |
| run: | | |
| git fetch origin main | |
| git checkout origin/main | |
| - name: Build and test main branch with coverage | |
| run: | | |
| dotnet build --configuration Release | |
| dotnet test \ | |
| --configuration Release \ | |
| --no-build \ | |
| --collect "XPlat Code Coverage" \ | |
| --settings CodeCoverage.runsettings \ | |
| --results-directory ./TestResults/Main \ | |
| -- DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.Format=cobertura | |
| continue-on-error: true | |
| - name: Merge main coverage reports | |
| run: | | |
| dotnet-coverage merge ./TestResults/Main/**/coverage.cobertura.xml \ | |
| --output ./TestResults/main-coverage.xml \ | |
| --output-format cobertura | |
| continue-on-error: true | |
| - name: Compare coverage and generate report | |
| id: coverage-compare | |
| shell: pwsh | |
| run: | | |
| $prCoverageFile = "./TestResults/pr-coverage.xml" | |
| $mainCoverageFile = "./TestResults/main-coverage.xml" | |
| function Get-ProjectCoverage { | |
| param([string]$CoverageFile) | |
| if (-not (Test-Path $CoverageFile)) { | |
| return @{} | |
| } | |
| [xml]$xml = Get-Content $CoverageFile | |
| $results = @{} | |
| foreach ($package in $xml.coverage.packages.package) { | |
| $name = $package.name | |
| $lineRate = [double]$package.'line-rate' * 100 | |
| $branchRate = [double]$package.'branch-rate' * 100 | |
| $lines = [int]$package.SelectNodes('.//line').Count | |
| $coveredLines = [int]$package.SelectNodes('.//line[@hits > 0]').Count | |
| $results[$name] = @{ | |
| LineRate = [math]::Round($lineRate, 2) | |
| BranchRate = [math]::Round($branchRate, 2) | |
| TotalLines = $lines | |
| CoveredLines = $coveredLines | |
| } | |
| } | |
| # Overall coverage | |
| $overallLineRate = [double]$xml.coverage.'line-rate' * 100 | |
| $overallBranchRate = [double]$xml.coverage.'branch-rate' * 100 | |
| $results['__OVERALL__'] = @{ | |
| LineRate = [math]::Round($overallLineRate, 2) | |
| BranchRate = [math]::Round($overallBranchRate, 2) | |
| } | |
| return $results | |
| } | |
| $prCoverage = Get-ProjectCoverage -CoverageFile $prCoverageFile | |
| $mainCoverage = Get-ProjectCoverage -CoverageFile $mainCoverageFile | |
| $markdown = "## π Code Coverage Comparison`n`n" | |
| # Overall comparison | |
| $prOverall = $prCoverage['__OVERALL__'] | |
| $mainOverall = $mainCoverage['__OVERALL__'] | |
| if ($mainOverall -and $prOverall) { | |
| $overallDiff = $prOverall.LineRate - $mainOverall.LineRate | |
| $diffIcon = if ($overallDiff -ge 0) { "π’" } elseif ($overallDiff -gt -1) { "π‘" } else { "π΄" } | |
| $diffSign = if ($overallDiff -ge 0) { "+" } else { "" } | |
| $markdown += "### Overall Coverage`n" | |
| $markdown += "| Metric | Main | PR | Diff |`n" | |
| $markdown += "|--------|------|-----|------|`n" | |
| $markdown += "| Line Coverage | $($mainOverall.LineRate)% | $($prOverall.LineRate)% | $diffIcon $diffSign$([math]::Round($overallDiff, 2))% |`n" | |
| $markdown += "| Branch Coverage | $($mainOverall.BranchRate)% | $($prOverall.BranchRate)% | $diffSign$([math]::Round($prOverall.BranchRate - $mainOverall.BranchRate, 2))% |`n`n" | |
| } | |
| # Projects with degradation | |
| $degraded = @() | |
| $improved = @() | |
| $allKeys = ($mainCoverage.Keys + $prCoverage.Keys) | Where-Object { $_ -ne '__OVERALL__' } | Sort-Object -Unique | |
| foreach ($key in $allKeys) { | |
| $main = $mainCoverage[$key] | |
| $pr = $prCoverage[$key] | |
| if ($null -eq $main -or $null -eq $pr) { continue } | |
| $diff = $pr.LineRate - $main.LineRate | |
| if ($diff -lt -1) { | |
| $degraded += @{ | |
| Name = $key | |
| MainRate = $main.LineRate | |
| PRRate = $pr.LineRate | |
| Diff = $diff | |
| } | |
| } | |
| elseif ($diff -gt 1) { | |
| $improved += @{ | |
| Name = $key | |
| MainRate = $main.LineRate | |
| PRRate = $pr.LineRate | |
| Diff = $diff | |
| } | |
| } | |
| } | |
| # Coverage degradations | |
| if ($degraded.Count -gt 0) { | |
| $markdown += "### β οΈ Coverage Degradation Detected`n`n" | |
| $markdown += "The following projects have significant coverage drops (>1%):`n`n" | |
| $markdown += "| Project | Main | PR | Change |`n" | |
| $markdown += "|---------|------|-----|--------|`n" | |
| foreach ($item in $degraded | Sort-Object Diff) { | |
| $shortName = $item.Name -replace 'Microsoft\.Health\.Fhir\.', '' | |
| $markdown += "| $shortName | $($item.MainRate)% | $($item.PRRate)% | π΄ $($item.Diff)% |`n" | |
| } | |
| $markdown += "`n" | |
| } | |
| # Coverage improvements | |
| if ($improved.Count -gt 0) { | |
| $markdown += "### β Coverage Improvements`n`n" | |
| $markdown += "| Project | Main | PR | Change |`n" | |
| $markdown += "|---------|------|-----|--------|`n" | |
| foreach ($item in $improved | Sort-Object Diff -Descending) { | |
| $shortName = $item.Name -replace 'Microsoft\.Health\.Fhir\.', '' | |
| $markdown += "| $shortName | $($item.MainRate)% | $($item.PRRate)% | π’ +$($item.Diff)% |`n" | |
| } | |
| $markdown += "`n" | |
| } | |
| if ($degraded.Count -eq 0 -and $improved.Count -eq 0) { | |
| $markdown += "β No significant coverage changes detected.`n" | |
| } | |
| $markdown += "`n---`n*Coverage comparison generated automatically.*" | |
| # Save to file for the comment step | |
| $markdown | Out-File -FilePath ./coverage-report.md -Encoding utf8 | |
| # Set output for degradation check | |
| $hasDegradation = $degraded.Count -gt 0 | |
| echo "has_degradation=$hasDegradation" >> $env:GITHUB_OUTPUT | |
| echo "degradation_count=$($degraded.Count)" >> $env:GITHUB_OUTPUT | |
| - name: Find existing coverage comment | |
| uses: peter-evans/find-comment@v3 | |
| id: find-comment | |
| with: | |
| issue-number: ${{ github.event.pull_request.number }} | |
| comment-author: 'github-actions[bot]' | |
| body-includes: 'π Code Coverage Comparison' | |
| - name: Post or update coverage comment | |
| uses: peter-evans/create-or-update-comment@v4 | |
| with: | |
| comment-id: ${{ steps.find-comment.outputs.comment-id }} | |
| issue-number: ${{ github.event.pull_request.number }} | |
| body-path: ./coverage-report.md | |
| edit-mode: replace | |
| - name: Fail if coverage degraded significantly | |
| if: steps.coverage-compare.outputs.has_degradation == 'true' | |
| run: | | |
| echo "::warning::Code coverage has degraded in ${{ steps.coverage-compare.outputs.degradation_count }} project(s)!" | |
| # Uncomment the next line to fail the build on coverage degradation | |
| # exit 1 |