diff --git a/eng/pipelines/release-cli.yml b/eng/pipelines/release-cli.yml index cdd05bfa4b6..2f5ac96c1a0 100644 --- a/eng/pipelines/release-cli.yml +++ b/eng/pipelines/release-cli.yml @@ -125,6 +125,7 @@ extends: - template: /eng/pipelines/templates/stages/code-coverage-upload.yml parameters: + MinimumCoveragePercent: 48 DownloadArtifacts: - cover-win - cover-lin diff --git a/eng/pipelines/templates/stages/code-coverage-upload.yml b/eng/pipelines/templates/stages/code-coverage-upload.yml index 84942e982fd..774a4c17f73 100644 --- a/eng/pipelines/templates/stages/code-coverage-upload.yml +++ b/eng/pipelines/templates/stages/code-coverage-upload.yml @@ -5,6 +5,9 @@ parameters: - cover-win - cover-lin - cover-mac + - name: MinimumCoveragePercent + type: number + default: 0 stages: - stage: CodeCoverage_Upload condition: and(succeeded(), ne(variables['Skip.LiveTest'], 'true')) @@ -59,7 +62,18 @@ stages: } displayName: Merge code coverage files + - ${{ if gt(parameters.MinimumCoveragePercent, 0) }}: + - task: PowerShell@2 + inputs: + pwsh: true + targetType: filePath + filePath: $(Build.SourcesDirectory)/eng/scripts/Test-CodeCoverageThreshold.ps1 + arguments: -CoverageFile $(Build.SourcesDirectory)/cover.out -MinimumCoveragePercent ${{ parameters.MinimumCoveragePercent }} + workingDirectory: $(Build.SourcesDirectory)/cli/azd + displayName: Check code coverage threshold + - task: PublishCodeCoverageResults@1 + condition: succeededOrFailed() inputs: codeCoverageTool: Cobertura summaryFileLocation: '$(Build.SourcesDirectory)/**/coverage.xml' diff --git a/eng/scripts/Test-CodeCoverageThreshold.ps1 b/eng/scripts/Test-CodeCoverageThreshold.ps1 new file mode 100644 index 00000000000..2ee1c2837a6 --- /dev/null +++ b/eng/scripts/Test-CodeCoverageThreshold.ps1 @@ -0,0 +1,67 @@ +#!/usr/bin/env pwsh + +<# +.SYNOPSIS + Checks that code coverage meets a minimum threshold. + +.DESCRIPTION + Parses a Go coverage profile using 'go tool cover -func' to extract the + total statement coverage percentage, then fails if it is below the + specified minimum. + +.PARAMETER CoverageFile + Path to the Go coverage profile (typically cover.out). + +.PARAMETER MinimumCoveragePercent + The minimum acceptable coverage percentage (0-100). The script exits + with a non-zero code when coverage is below this value. + +.EXAMPLE + ./Test-CodeCoverageThreshold.ps1 -CoverageFile cover.out -MinimumCoveragePercent 48 +#> + +param( + [Parameter(Mandatory = $true)] + [string]$CoverageFile, + + [Parameter(Mandatory = $true)] + [ValidateRange(0, 100)] + [double]$MinimumCoveragePercent +) + +$ErrorActionPreference = 'Stop' + +if (-not (Test-Path $CoverageFile)) { + throw "Coverage file '$CoverageFile' not found" +} + +Write-Host "Checking code coverage threshold (minimum: $MinimumCoveragePercent%)..." + +# Use 'go tool cover -func' to get per-function coverage and the total line. +$output = & go tool cover "-func=$CoverageFile" +if ($LASTEXITCODE) { + throw "go tool cover -func failed: $output" +} + +# Find the "total:" summary line, which looks like: "total: (statements) 48.3%" +$totalLine = $output | Where-Object { $_ -match '^\s*total:' } | Select-Object -Last 1 + +if (-not $totalLine) { + throw "Could not find 'total:' line in 'go tool cover -func' output" +} + +# Match both integer (100%) and decimal (48.3%) percentages using invariant culture +if ($totalLine -match '(\d+(?:\.\d+)?)%') { + $coveragePercent = [double]::Parse($matches[1], [System.Globalization.CultureInfo]::InvariantCulture) +} else { + throw "Could not parse coverage percentage from: $totalLine" +} + +Write-Host "Total statement coverage: $coveragePercent%" + +if ($coveragePercent -lt $MinimumCoveragePercent) { + Write-Host "##vso[task.logissue type=error]Code coverage $coveragePercent% is below the minimum threshold of $MinimumCoveragePercent%." + exit 1 +} + +Write-Host "Coverage $coveragePercent% meets the minimum threshold of $MinimumCoveragePercent%. ✓"