FEAT: Complex Datatype support-XML #131
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: 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 | |
with: | |
fetch-depth: 0 | |
- name: Setup git for diff-cover | |
run: | | |
# Fetch the main branch for comparison | |
git fetch origin main:main | |
# Show available branches for debugging | |
echo "Available branches:" | |
git branch -a | |
# Verify main branch exists | |
git show-ref --verify refs/heads/main || echo "Warning: main branch not found" | |
git show-ref --verify refs/remotes/origin/main || echo "Warning: origin/main not found" | |
- name: Wait for ADO build to start | |
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 to start for PR #$PR_NUMBER ..." | |
for i in {1..30}; do | |
echo "Attempt $i/30: Checking if build has started..." | |
# 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" | |
echo "🔗 Build URL: $WEB_URL" | |
echo "ADO_URL=$WEB_URL" >> $GITHUB_ENV | |
echo "BUILD_ID=$BUILD_ID" >> $GITHUB_ENV | |
# Check if build has failed early | |
if [[ "$STATUS" == "completed" && "$RESULT" == "failed" ]]; then | |
echo "❌ Azure DevOps build $BUILD_ID failed early" | |
echo "This coverage workflow cannot proceed when the main build fails." | |
exit 1 | |
fi | |
echo "🚀 Build has started, proceeding to poll for coverage artifacts..." | |
break | |
else | |
echo "⏳ No build found for PR #$PR_NUMBER yet... (attempt $i/30)" | |
fi | |
if [[ $i -eq 30 ]]; then | |
echo "❌ Timeout: No build found for PR #$PR_NUMBER after 30 attempts" | |
echo "This may indicate the Azure DevOps pipeline was not triggered" | |
exit 1 | |
fi | |
sleep 10 | |
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 "📥 Polling for coverage artifacts for build $BUILD_ID..." | |
# Poll for coverage artifacts with retry logic | |
COVERAGE_ARTIFACT="" | |
for i in {1..60}; do | |
echo "Attempt $i/60: Checking for coverage artifacts..." | |
# 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 (attempt $i/60)" | |
if [[ $i -eq 60 ]]; then | |
echo "❌ Persistent API issues after 60 attempts" | |
echo "Response received: $ARTIFACTS_RESPONSE" | |
exit 1 | |
fi | |
sleep 30 | |
continue | |
fi | |
# Show available artifacts for debugging | |
echo "🔍 Available artifacts:" | |
echo "$ARTIFACTS_RESPONSE" | jq -r '.value[]?.name // "No artifacts found"' | |
# 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 "✅ Found coverage artifact on attempt $i!" | |
break | |
else | |
echo "⏳ Coverage report not ready yet (attempt $i/60)..." | |
if [[ $i -eq 60 ]]; then | |
echo "❌ Timeout: Coverage report artifact not found after 60 attempts" | |
echo "Available artifacts:" | |
echo "$ARTIFACTS_RESPONSE" | jq -r '.value[]?.name // "No artifacts found"' | |
exit 1 | |
fi | |
sleep 30 | |
fi | |
done | |
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 -o -q coverage-report.zip; then | |
echo "❌ Failed to extract coverage artifacts" | |
echo "Trying to extract with verbose output for debugging..." | |
unzip -l coverage-report.zip || echo "Failed to list archive contents" | |
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: 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 -o -q coverage-artifacts.zip; then | |
echo "❌ Failed to extract coverage artifacts" | |
echo "Trying to extract with verbose output for debugging..." | |
unzip -l coverage-artifacts.zip || echo "Failed to list archive contents" | |
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..." | |
# Debug: Show git status and branches before running diff-cover | |
echo "🔍 Git status before diff-cover:" | |
git status --porcelain || echo "Git status failed" | |
echo "Current branch: $(git branch --show-current)" | |
echo "Available branches:" | |
git branch -a | |
echo "Checking if main branch is accessible:" | |
git log --oneline -n 5 main || echo "Could not access main branch" | |
# Debug: Show what diff-cover will analyze | |
echo "🔍 Git diff analysis:" | |
echo "Files changed between main and current branch:" | |
git diff --name-only main || echo "Could not get diff" | |
echo "Detailed diff for Python files:" | |
git diff main -- "*.py" | head -50 || echo "Could not get Python diff" | |
# Debug: Check coverage.xml content for specific files | |
echo "🔍 Coverage.xml analysis:" | |
echo "Python files mentioned in coverage.xml:" | |
grep -o 'filename="[^"]*\.py"' coverage.xml | head -10 || echo "Could not extract filenames" | |
echo "Sample coverage data:" | |
head -20 coverage.xml | |
# Use the new format for diff-cover commands | |
echo "🚀 Running diff-cover..." | |
diff-cover coverage.xml \ | |
--compare-branch=main \ | |
--html-report patch-coverage.html \ | |
--json-report patch-coverage.json \ | |
--markdown-report 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" | |
echo "🔍 diff-cover verbose output:" | |
diff-cover coverage.xml --compare-branch=main --markdown-report debug-patch-coverage.md -v || echo "Verbose diff-cover also failed" | |
fi | |
# Don't exit here, let's see what files were created | |
} | |
# Check what files were generated | |
echo "🔍 Files generated after diff-cover:" | |
ls -la patch-coverage.* || echo "No patch-coverage files found" | |
ls -la *.md *.html *.json | grep -E "(patch|coverage)" || echo "No coverage-related files found" | |
# Extract patch coverage percentage | |
if [[ -f patch-coverage.json ]]; then | |
echo "🔍 Patch coverage analysis from JSON:" | |
echo "Raw JSON content:" | |
cat patch-coverage.json | jq . || echo "Could not parse JSON" | |
PATCH_COVERAGE=$(jq -r '.total_percent_covered // "N/A"' patch-coverage.json) | |
TOTAL_STATEMENTS=$(jq -r '.total_num_lines // "N/A"' patch-coverage.json) | |
MISSING_STATEMENTS=$(jq -r '.total_num_missing // "N/A"' patch-coverage.json) | |
echo "✅ Patch coverage: ${PATCH_COVERAGE}%" | |
echo "📊 Total lines: $TOTAL_STATEMENTS, Missing: $MISSING_STATEMENTS" | |
# Debug: Show per-file breakdown | |
echo "📁 Per-file coverage breakdown:" | |
jq -r '.src_stats // {} | to_entries[] | "\(.key): \(.value.percent_covered)% (\(.value.num_lines) lines, \(.value.num_missing) missing)"' patch-coverage.json || echo "Could not extract per-file stats" | |
echo "PATCH_COVERAGE_PCT=${PATCH_COVERAGE}%" >> $GITHUB_ENV | |
elif [[ -f patch-coverage.md ]]; then | |
echo "🔍 Extracting patch coverage from markdown file:" | |
echo "Markdown content:" | |
cat patch-coverage.md | |
# Extract coverage percentage from markdown | |
PATCH_COVERAGE=$(grep -o "Coverage.*[0-9]*%" patch-coverage.md | grep -o "[0-9]*%" | head -1 | sed 's/%//') | |
TOTAL_LINES=$(grep -o "Total.*[0-9]* lines" patch-coverage.md | grep -o "[0-9]*" | head -1) | |
MISSING_LINES=$(grep -o "Missing.*[0-9]* lines" patch-coverage.md | grep -o "[0-9]*" | tail -1) | |
if [[ -n "$PATCH_COVERAGE" ]]; then | |
echo "✅ Extracted patch coverage: ${PATCH_COVERAGE}%" | |
echo "📊 Total lines: $TOTAL_LINES, Missing: $MISSING_LINES" | |
echo "PATCH_COVERAGE_PCT=${PATCH_COVERAGE}%" >> $GITHUB_ENV | |
else | |
echo "⚠️ Could not extract coverage percentage from markdown" | |
echo "PATCH_COVERAGE_PCT=Could not parse" >> $GITHUB_ENV | |
fi | |
else | |
echo "⚠️ No patch coverage files generated" | |
echo "🔍 Checking for other output files:" | |
ls -la *coverage* || echo "No coverage files found" | |
echo "PATCH_COVERAGE_PCT=Report not generated" >> $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"> | |
### 🔥 Diff Coverage | |
### **${{ env.PATCH_COVERAGE_PCT }}** | |
<br> | |
</td> | |
<td align="center" width="200"> | |
### 🎯 Overall Coverage | |
### **${{ env.COVERAGE_PERCENTAGE }}** | |
<br> | |
</td> | |
<td> | |
**📈 Total Lines Covered:** `${{ env.COVERED_LINES }}` out of `${{ env.TOTAL_LINES }}` | |
**📁 Project:** `mssql-python` | |
</td> | |
</tr> | |
</table> | |
--- | |
${{ env.PATCH_COVERAGE_SUMMARY }} | |
--- | |
### 📋 Files Needing Attention | |
<details> | |
<summary>📉 <strong>Files with overall 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> |