Skip to content

FEAT: Full Code Coverage & Workflow Integration #22

FEAT: Full Code Coverage & Workflow Integration

FEAT: Full Code Coverage & Workflow Integration #22

name: PR Code Coverage
on:
pull_request:
branches:
- main
jobs:
coverage-report:
runs-on: ubuntu-latest
permissions:
pull-requests: write
contents: read
steps:
- name: Checkout repo
uses: actions/checkout@v4
- name: Wait for ADO build to succeed
run: |
PR_NUMBER=${{ github.event.pull_request.number }}
API_URL="https://dev.azure.com/sqlclientdrivers/public/_apis/build/builds?definitions=2128&queryOrder=queueTimeDescending&%24top=10&api-version=7.1-preview.7"
echo "Waiting for Azure DevOps build for PR #$PR_NUMBER ..."
for i in {1..100}; do
echo "Attempt $i/100: Checking build status..."
# Fetch API response with error handling
API_RESPONSE=$(curl -s "$API_URL")
# Check if response is valid JSON
if ! echo "$API_RESPONSE" | jq . >/dev/null 2>&1; then
echo "❌ Invalid JSON response from Azure DevOps API"
echo "Response received: $API_RESPONSE"
echo "This usually indicates the Azure DevOps pipeline has failed or API is unavailable"
exit 1
fi
# Parse build info safely
BUILD_INFO=$(echo "$API_RESPONSE" | jq -c --arg PR "$PR_NUMBER" '[.value[]? | select(.triggerInfo["pr.number"]?==$PR)] | .[0] // empty' 2>/dev/null)
if [[ -n "$BUILD_INFO" && "$BUILD_INFO" != "null" && "$BUILD_INFO" != "empty" ]]; then
STATUS=$(echo "$BUILD_INFO" | jq -r '.status // "unknown"')
RESULT=$(echo "$BUILD_INFO" | jq -r '.result // "unknown"')
BUILD_ID=$(echo "$BUILD_INFO" | jq -r '.id // "unknown"')
WEB_URL=$(echo "$BUILD_INFO" | jq -r '._links.web.href // "unknown"')
echo "Found build: ID=$BUILD_ID, Status=$STATUS, Result=$RESULT"
if [[ "$STATUS" == "completed" ]]; then
if [[ "$RESULT" == "succeeded" ]]; then
echo "βœ… Build $BUILD_ID succeeded: $WEB_URL"
echo "ADO_URL=$WEB_URL" >> $GITHUB_ENV
echo "BUILD_ID=$BUILD_ID" >> $GITHUB_ENV
break
else
echo "❌ Azure DevOps build $BUILD_ID failed with result: $RESULT"
echo "πŸ”— Build URL: $WEB_URL"
echo "This coverage workflow cannot proceed when the main build fails."
exit 1
fi
else
echo "⏳ Build $BUILD_ID is still $STATUS..."
fi
else
echo "⏳ No build found for PR #$PR_NUMBER yet... (attempt $i/100)"
fi
if [[ $i -eq 100 ]]; then
echo "❌ Timeout: No build found for PR #$PR_NUMBER after 100 attempts"
echo "This may indicate the Azure DevOps pipeline was not triggered or failed to start"
exit 1
fi
sleep 20
done
- name: Download and parse coverage report
run: |
BUILD_ID=${{ env.BUILD_ID }}
ARTIFACTS_URL="https://dev.azure.com/SqlClientDrivers/public/_apis/build/builds/$BUILD_ID/artifacts?api-version=7.1-preview.5"
echo "πŸ“₯ Fetching artifacts for build $BUILD_ID..."
# Fetch artifacts with error handling
ARTIFACTS_RESPONSE=$(curl -s "$ARTIFACTS_URL")
# Check if response is valid JSON
if ! echo "$ARTIFACTS_RESPONSE" | jq . >/dev/null 2>&1; then
echo "❌ Invalid JSON response from artifacts API"
echo "Response received: $ARTIFACTS_RESPONSE"
echo "This indicates the Azure DevOps build may not have completed successfully or artifacts are not available"
exit 1
fi
# Find the coverage report artifact
COVERAGE_ARTIFACT=$(echo "$ARTIFACTS_RESPONSE" | jq -r '.value[]? | select(.name | test("Code Coverage Report")) | .resource.downloadUrl // empty' 2>/dev/null)
if [[ -n "$COVERAGE_ARTIFACT" && "$COVERAGE_ARTIFACT" != "null" && "$COVERAGE_ARTIFACT" != "empty" ]]; then
echo "πŸ“Š Downloading coverage report..."
if ! curl -L "$COVERAGE_ARTIFACT" -o coverage-report.zip --fail --silent; then
echo "❌ Failed to download coverage report from Azure DevOps"
echo "This indicates the coverage artifacts may not be available or accessible"
exit 1
fi
if ! unzip -q coverage-report.zip; then
echo "❌ Failed to extract coverage report zip file"
echo "The downloaded artifact may be corrupted"
exit 1
fi
# Find the main index.html file
INDEX_FILE=$(find . -name "index.html" -path "*/Code Coverage Report*" | head -1)
if [[ -f "$INDEX_FILE" ]]; then
echo "πŸ” Parsing coverage data from $INDEX_FILE..."
# Debug: Show relevant parts of the HTML
echo "Debug: Looking for coverage data..."
grep -n "cardpercentagebar\|Covered lines\|Coverable lines" "$INDEX_FILE" | head -10
# Extract coverage metrics using simpler, more reliable patterns
OVERALL_PERCENTAGE=$(grep -o 'cardpercentagebar[0-9]*">[0-9]*%' "$INDEX_FILE" | head -1 | grep -o '[0-9]*%')
COVERED_LINES=$(grep -A1 "Covered lines:" "$INDEX_FILE" | grep -o 'title="[0-9]*"' | head -1 | grep -o '[0-9]*')
TOTAL_LINES=$(grep -A1 "Coverable lines:" "$INDEX_FILE" | grep -o 'title="[0-9]*"' | head -1 | grep -o '[0-9]*')
# Fallback method if the above doesn't work
if [[ -z "$OVERALL_PERCENTAGE" ]]; then
echo "Trying alternative parsing method..."
OVERALL_PERCENTAGE=$(grep -o 'large.*">[0-9]*%' "$INDEX_FILE" | head -1 | grep -o '[0-9]*%')
fi
echo "Extracted values:"
echo "OVERALL_PERCENTAGE=$OVERALL_PERCENTAGE"
echo "COVERED_LINES=$COVERED_LINES"
echo "TOTAL_LINES=$TOTAL_LINES"
# Validate that we got the essential data
if [[ -z "$OVERALL_PERCENTAGE" ]]; then
echo "❌ Could not extract coverage percentage from the report"
echo "The coverage report format may have changed or be incomplete"
exit 1
fi
echo "COVERAGE_PERCENTAGE=$OVERALL_PERCENTAGE" >> $GITHUB_ENV
echo "COVERED_LINES=${COVERED_LINES:-N/A}" >> $GITHUB_ENV
echo "TOTAL_LINES=${TOTAL_LINES:-N/A}" >> $GITHUB_ENV
# Extract top files with low coverage - improved approach
echo "πŸ“‹ Extracting file-level coverage..."
# Extract file coverage data more reliably
LOW_COVERAGE_FILES=$(grep -o '<td><a href="[^"]*">[^<]*</a></td><td class="right">[0-9]*</td><td class="right">[0-9]*</td><td class="right">[0-9]*</td><td class="right">[0-9]*</td><td title="[^"]*" class="right">[0-9]*\.[0-9]*%' "$INDEX_FILE" | \
sed 's/<td><a href="[^"]*">\([^<]*\)<\/a><\/td>.*class="right">\([0-9]*\.[0-9]*\)%/\1: \2%/' | \
sort -t: -k2 -n | head -10)
# Alternative method if above fails
if [[ -z "$LOW_COVERAGE_FILES" ]]; then
echo "Trying alternative file parsing..."
LOW_COVERAGE_FILES=$(grep -E "\.py.*[0-9]+\.[0-9]+%" "$INDEX_FILE" | \
grep -o "[^>]*\.py[^<]*</a>.*[0-9]*\.[0-9]*%" | \
sed 's/\([^<]*\)<\/a>.*\([0-9]*\.[0-9]*\)%/\1: \2%/' | \
sort -t: -k2 -n | head -10)
fi
echo "LOW_COVERAGE_FILES<<EOF" >> $GITHUB_ENV
echo "${LOW_COVERAGE_FILES:-No detailed file data available}" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
echo "βœ… Coverage data extracted successfully"
else
echo "❌ Could not find index.html in coverage report"
echo "Available files in the coverage report:"
find . -name "*.html" | head -10 || echo "No HTML files found"
exit 1
fi
else
echo "❌ Could not find coverage report artifact"
echo "Available artifacts from the build:"
echo "$ARTIFACTS_RESPONSE" | jq -r '.value[]?.name // "No artifacts found"' 2>/dev/null || echo "Could not parse artifacts list"
echo "This indicates the Azure DevOps build may not have generated coverage reports"
exit 1
fi
# - name: Comment coverage summary on PR
# uses: marocchino/sticky-pull-request-comment@v2
# with:
# header: Code Coverage Report
# message: |
# # πŸ“Š Code Coverage Report
# <table>
# <tr>
# <td align="center" width="200">
# ### 🎯 Coverage
# ### **${{ env.COVERAGE_PERCENTAGE }}**
# <br>
# </td>
# <td>
# **πŸ“ˆ Lines Covered:** `${{ env.COVERED_LINES }}` out of `${{ env.TOTAL_LINES }}`
# **πŸ“ Project:** `mssql-python`
# </td>
# </tr>
# </table>
# ---
# ### πŸ“‹ Files Needing Attention
# <details>
# <summary>πŸ“‰ <strong>Files with lowest coverage</strong> (click to expand)</summary>
# <br>
# ```diff
# ${{ env.LOW_COVERAGE_FILES }}
# ```
# </details>
# ---
# ### πŸ”— Quick Links
# <table>
# <tr>
# <td align="left" width="200">
# <b>βš™οΈ Build Summary</b>
# </td>
# <td align="left">
# <b>πŸ“‹ Coverage Details</b>
# </td>
# </tr>
# <tr>
# <td align="left" width="200">
# [View Azure DevOps Build](${{ env.ADO_URL }})
# </td>
# <td align="left">
# [Browse Full Coverage Report](${{ env.ADO_URL }}&view=codecoverage-tab)
# </td>
# </tr>
# </table>
- name: Download coverage XML from ADO
run: |
# Download the Cobertura XML directly from the CodeCoverageReport job
BUILD_ID=${{ env.BUILD_ID }}
ARTIFACTS_URL="https://dev.azure.com/SqlClientDrivers/public/_apis/build/builds/$BUILD_ID/artifacts?api-version=7.1-preview.5"
echo "πŸ“₯ Fetching artifacts for build $BUILD_ID to find coverage files..."
# Fetch artifacts with error handling
ARTIFACTS_RESPONSE=$(curl -s "$ARTIFACTS_URL")
# Check if response is valid JSON
if ! echo "$ARTIFACTS_RESPONSE" | jq . >/dev/null 2>&1; then
echo "❌ Invalid JSON response from artifacts API"
echo "Response received: $ARTIFACTS_RESPONSE"
exit 1
fi
echo "πŸ” Available artifacts:"
echo "$ARTIFACTS_RESPONSE" | jq -r '.value[]?.name // "No artifacts found"'
# Look for the unified coverage artifact from CodeCoverageReport job
COVERAGE_XML_ARTIFACT=$(echo "$ARTIFACTS_RESPONSE" | jq -r '.value[]? | select(.name | test("unified-coverage|Code Coverage Report|coverage")) | .resource.downloadUrl // empty' 2>/dev/null | head -1)
if [[ -n "$COVERAGE_XML_ARTIFACT" && "$COVERAGE_XML_ARTIFACT" != "null" && "$COVERAGE_XML_ARTIFACT" != "empty" ]]; then
echo "πŸ“Š Downloading coverage artifact from: $COVERAGE_XML_ARTIFACT"
if ! curl -L "$COVERAGE_XML_ARTIFACT" -o coverage-artifacts.zip --fail --silent; then
echo "❌ Failed to download coverage artifacts"
exit 1
fi
if ! unzip -q coverage-artifacts.zip; then
echo "❌ Failed to extract coverage artifacts"
exit 1
fi
echo "πŸ” Looking for coverage XML files in extracted artifacts..."
find . -name "*.xml" -type f | head -10
# Look for the main coverage.xml file in unified-coverage directory or any coverage XML
if [[ -f "unified-coverage/coverage.xml" ]]; then
echo "βœ… Found unified coverage file at unified-coverage/coverage.xml"
cp "unified-coverage/coverage.xml" ./coverage.xml
elif [[ -f "coverage.xml" ]]; then
echo "βœ… Found coverage.xml in root directory"
# Already in the right place
else
# Try to find any coverage XML file
COVERAGE_FILE=$(find . -name "*coverage*.xml" -type f | head -1)
if [[ -n "$COVERAGE_FILE" ]]; then
echo "βœ… Found coverage file: $COVERAGE_FILE"
cp "$COVERAGE_FILE" ./coverage.xml
else
echo "❌ No coverage XML file found in artifacts"
echo "Available files:"
find . -name "*.xml" -type f
exit 1
fi
fi
echo "βœ… Coverage XML file is ready at ./coverage.xml"
ls -la ./coverage.xml
else
echo "❌ Could not find coverage artifacts"
echo "This indicates the Azure DevOps CodeCoverageReport job may not have run successfully"
exit 1
fi
- name: Generate patch coverage report
run: |
# Install dependencies
pip install diff-cover jq
sudo apt-get update && sudo apt-get install -y libxml2-utils
# Verify coverage.xml exists before proceeding
if [[ ! -f coverage.xml ]]; then
echo "❌ coverage.xml not found in current directory"
echo "Available files:"
ls -la | head -20
exit 1
fi
echo "βœ… coverage.xml found, size: $(wc -c < coverage.xml) bytes"
echo "πŸ” Coverage file preview (first 10 lines):"
head -10 coverage.xml
# Generate diff coverage report using the new command format
echo "πŸš€ Generating patch coverage report..."
# Use the new format for diff-cover commands
diff-cover coverage.xml \
--compare-branch=origin/main \
--format html:patch-coverage.html \
--format json:patch-coverage.json \
--format markdown:patch-coverage.md || {
echo "❌ diff-cover failed with exit code $?"
echo "Checking if coverage.xml is valid XML..."
if ! xmllint --noout coverage.xml 2>/dev/null; then
echo "❌ coverage.xml is not valid XML"
echo "First 50 lines of coverage.xml:"
head -50 coverage.xml
else
echo "βœ… coverage.xml is valid XML"
fi
exit 1
}
# Extract patch coverage percentage
if [[ -f patch-coverage.json ]]; then
PATCH_COVERAGE=$(jq -r '.total_percent_covered // "N/A"' patch-coverage.json)
echo "PATCH_COVERAGE_PCT=${PATCH_COVERAGE}%" >> $GITHUB_ENV
echo "βœ… Patch coverage: ${PATCH_COVERAGE}%"
else
echo "⚠️ patch-coverage.json not generated, setting default"
echo "PATCH_COVERAGE_PCT=N/A" >> $GITHUB_ENV
fi
# Extract summary for comment
if [[ -f patch-coverage.md ]]; then
echo "PATCH_COVERAGE_SUMMARY<<EOF" >> $GITHUB_ENV
cat patch-coverage.md >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
echo "βœ… Patch coverage markdown summary ready"
else
echo "⚠️ patch-coverage.md not generated"
echo "PATCH_COVERAGE_SUMMARY=Patch coverage report could not be generated." >> $GITHUB_ENV
fi
- name: Comment coverage summary on PR
uses: marocchino/sticky-pull-request-comment@v2
with:
header: Code Coverage Report
message: |
# πŸ“Š Code Coverage Report
<table>
<tr>
<td align="center" width="200">
### 🎯 Overall Coverage
### **${{ env.COVERAGE_PERCENTAGE }}**
<br>
</td>
<td align="center" width="200">
### πŸ”₯ Patch Coverage
### **${{ env.PATCH_COVERAGE_PCT }}**
<br>
</td>
<td>
**πŸ“ˆ Lines Covered:** `${{ env.COVERED_LINES }}` out of `${{ env.TOTAL_LINES }}`
**πŸ“ Project:** `mssql-python`
</td>
</tr>
</table>
---
### πŸ“‹ Files Needing Attention
<details>
<summary>🎯 <strong>Patch Coverage Details</strong> (lines changed in this PR)</summary>
<br>
> **Patch Coverage** shows the percentage of **newly added or modified lines** that are covered by tests.
${{ env.PATCH_COVERAGE_SUMMARY }}
</details>
<details>
<summary>πŸ“‰ <strong>Files with lowest coverage</strong> (click to expand)</summary>
<br>
```diff
${{ env.LOW_COVERAGE_FILES }}
```
</details>
---
### πŸ”— Quick Links
<table>
<tr>
<td align="left" width="200">
<b>βš™οΈ Build Summary</b>
</td>
<td align="left">
<b>πŸ“‹ Coverage Details</b>
</td>
</tr>
<tr>
<td align="left" width="200">
[View Azure DevOps Build](${{ env.ADO_URL }})
</td>
<td align="left">
[Browse Full Coverage Report](${{ env.ADO_URL }}&view=codecoverage-tab)
</td>
</tr>
</table>