FEAT: Full Code Coverage & Workflow Integration #22
  
    
      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 | |
| - 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> |