Skip to content

Build(deps): Bump urllib3 from 2.7.0 to 2.8.0 in /eng #2983

Build(deps): Bump urllib3 from 2.7.0 to 2.8.0 in /eng

Build(deps): Bump urllib3 from 2.7.0 to 2.8.0 in /eng #2983

name: PR Code Coverage
on:
pull_request:
branches:
- main
permissions:
contents: read
concurrency:
group: pr-code-coverage-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
coverage-report:
runs-on: ubuntu-latest
timeout-minutes: 245
permissions:
pull-requests: write
contents: read
steps:
- name: Checkout repo
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
fetch-depth: 0
persist-credentials: false
- 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
env:
PR_NUMBER: ${{ github.event.pull_request.number }}
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
PR_BRANCH="refs/pull/$PR_NUMBER/merge"
API_URL="https://dev.azure.com/sqlclientdrivers/public/_apis/build/builds?definitions=2128&branchName=refs%2Fpull%2F${PR_NUMBER}%2Fmerge&queryOrder=queueTimeDescending&%24top=100&api-version=7.1-preview.7"
DEADLINE=$((SECONDS + 15 * 60))
API_FAILURES=0
BUILD_ID=""
echo "Waiting up to 15 minutes for Azure DevOps build for PR #$PR_NUMBER at $PR_HEAD_SHA..."
while (( SECONDS < DEADLINE )); do
REQUEST_TIMEOUT=$((DEADLINE - SECONDS))
if (( REQUEST_TIMEOUT <= 0 )); then break; fi
if (( REQUEST_TIMEOUT > 30 )); then REQUEST_TIMEOUT=30; fi
if API_RESPONSE=$(curl --fail --silent --show-error --connect-timeout 10 --max-time "$REQUEST_TIMEOUT" "$API_URL") &&
jq -e 'type == "object" and (.value | type == "array")' <<< "$API_RESPONSE" >/dev/null 2>&1; then
API_FAILURES=0
else
API_FAILURES=$((API_FAILURES + 1))
echo "⚠️ Build API HTTP/JSON error ($API_FAILURES/5)"
if (( API_FAILURES >= 5 )); then
echo "❌ Azure DevOps build API unavailable after 5 consecutive failures"
exit 1
fi
API_RESPONSE='{"value":[]}'
fi
# The merge ref is shared across revisions; match the actual PR head as well.
BUILD_INFO=$(jq -c --arg PR "$PR_NUMBER" --arg SHA "$PR_HEAD_SHA" --arg BRANCH "$PR_BRANCH" '
[.value[]? | select(
.definition.id == 2128 and .sourceBranch == $BRANCH and
(.triggerInfo["pr.number"] | tostring) == $PR and
.triggerInfo["pr.sourceSha"] == $SHA
)] | .[0] // empty' <<< "$API_RESPONSE")
if [[ -n "$BUILD_INFO" ]]; 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"')
[[ "$BUILD_ID" =~ ^[0-9]+$ ]] || {
echo "Invalid Azure DevOps build ID"
exit 1
}
if [[ ${#WEB_URL} -gt 500 || "$WEB_URL" == *$'\n'* || "$WEB_URL" == *$'\r'* ]]; then
echo "Invalid Azure DevOps build URL"
exit 1
fi
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"
echo "PR_NUMBER=$PR_NUMBER" >> "$GITHUB_ENV"
echo "PR_HEAD_SHA=$PR_HEAD_SHA" >> "$GITHUB_ENV"
# A failed matrix leg does not invalidate a successful coverage artifact.
echo "πŸš€ Build found, proceeding to poll for coverage artifacts..."
break
fi
echo "⏳ No matching build found for PR #$PR_NUMBER at $PR_HEAD_SHA yet..."
SLEEP_SECONDS=$((DEADLINE - SECONDS))
if (( SLEEP_SECONDS > 30 )); then SLEEP_SECONDS=30; fi
if (( SLEEP_SECONDS > 0 )); then sleep "$SLEEP_SECONDS"; fi
done
if [[ -z "$BUILD_ID" ]]; then
echo "❌ Timeout: No build found for PR #$PR_NUMBER at $PR_HEAD_SHA within 15 minutes"
exit 1
fi
- name: Download and parse coverage report
run: |
BUILD_URL="https://dev.azure.com/SqlClientDrivers/public/_apis/build/builds/$BUILD_ID?api-version=7.1-preview.7"
ARTIFACTS_URL="https://dev.azure.com/SqlClientDrivers/public/_apis/build/builds/$BUILD_ID/artifacts?api-version=7.1-preview.5"
BUILDS_URL="https://dev.azure.com/sqlclientdrivers/public/_apis/build/builds?definitions=2128&branchName=refs%2Fpull%2F${PR_NUMBER}%2Fmerge&queryOrder=queueTimeDescending&%24top=100&api-version=7.1-preview.7"
PR_BRANCH="refs/pull/$PR_NUMBER/merge"
# Coverage may queue behind 160-minute benchmark jobs before its own run.
DEADLINE=$((SECONDS + 220 * 60))
COMPLETED_AT=-1
ARTIFACT_FAILURES=0
BUILD_FAILURES=0
COVERAGE_ARTIFACT=""
COVERAGE_ARTIFACT_APPROVED=false
echo "πŸ“₯ Waiting up to 220 minutes for coverage artifacts for build $BUILD_ID..."
while (( SECONDS < DEADLINE )); do
COVERAGE_ARTIFACT=""
REQUEST_TIMEOUT=$((DEADLINE - SECONDS))
if (( REQUEST_TIMEOUT <= 0 )); then break; fi
if (( REQUEST_TIMEOUT > 30 )); then REQUEST_TIMEOUT=30; fi
if ARTIFACTS_RESPONSE=$(curl --fail --silent --show-error --connect-timeout 10 --max-time "$REQUEST_TIMEOUT" "$ARTIFACTS_URL") &&
jq -e 'type == "object" and (.value | type == "array")' <<< "$ARTIFACTS_RESPONSE" >/dev/null 2>&1; then
ARTIFACT_FAILURES=0
COVERAGE_ARTIFACT=$(jq -r '
[.value[]? | select(.name | test("Code Coverage Report")) |
.resource.downloadUrl | select(type == "string" and length > 0)] |
.[0] // empty' <<< "$ARTIFACTS_RESPONSE")
else
ARTIFACT_FAILURES=$((ARTIFACT_FAILURES + 1))
echo "⚠️ Artifacts API HTTP/JSON error ($ARTIFACT_FAILURES/5)"
if (( ARTIFACT_FAILURES >= 5 )); then
echo "❌ Azure DevOps artifacts API unavailable after 5 consecutive failures"
exit 1
fi
fi
# Inspect lifecycle, not aggregate result: independent matrix jobs can fail.
REQUEST_TIMEOUT=$((DEADLINE - SECONDS))
if (( REQUEST_TIMEOUT <= 0 )); then break; fi
if (( REQUEST_TIMEOUT > 30 )); then REQUEST_TIMEOUT=30; fi
if BUILD_RESPONSE=$(curl --fail --silent --show-error --connect-timeout 10 --max-time "$REQUEST_TIMEOUT" "$BUILD_URL") &&
jq -e --arg ID "$BUILD_ID" '(.id | tostring) == $ID and
(.status | type == "string")' <<< "$BUILD_RESPONSE" >/dev/null 2>&1; then
BUILD_FAILURES=0
STATUS=$(jq -r '.status' <<< "$BUILD_RESPONSE")
RESULT=$(jq -r '.result // "unknown"' <<< "$BUILD_RESPONSE")
if [[ "$STATUS" == "completed" && "$RESULT" == "canceled" ]]; then
if REPLACEMENTS=$(curl --fail --silent --show-error --connect-timeout 10 --max-time "$REQUEST_TIMEOUT" "$BUILDS_URL") &&
REPLACEMENT=$(jq -ce --arg PR "$PR_NUMBER" --arg SHA "$PR_HEAD_SHA" \
--arg BRANCH "$PR_BRANCH" --arg ID "$BUILD_ID" '
[.value[]? | select(
.definition.id == 2128 and .sourceBranch == $BRANCH and
(.triggerInfo["pr.number"] | tostring) == $PR and
.triggerInfo["pr.sourceSha"] == $SHA and
.id > ($ID | tonumber) and
(.status != "completed" or .result != "canceled")
)] | .[0]' <<< "$REPLACEMENTS"); then
BUILD_ID=$(jq -r '.id' <<< "$REPLACEMENT")
[[ "$BUILD_ID" =~ ^[0-9]+$ ]] || {
echo "Invalid replacement Azure DevOps build ID"
exit 1
}
BUILD_URL="https://dev.azure.com/SqlClientDrivers/public/_apis/build/builds/$BUILD_ID?api-version=7.1-preview.7"
ARTIFACTS_URL="https://dev.azure.com/SqlClientDrivers/public/_apis/build/builds/$BUILD_ID/artifacts?api-version=7.1-preview.5"
ADO_URL="https://dev.azure.com/sqlclientdrivers/public/_build/results?buildId=$BUILD_ID"
echo "BUILD_ID=$BUILD_ID" >> "$GITHUB_ENV"
echo "ADO_URL=$ADO_URL" >> "$GITHUB_ENV"
COMPLETED_AT=-1
ARTIFACT_FAILURES=0
BUILD_FAILURES=0
COVERAGE_ARTIFACT=""
COVERAGE_ARTIFACT_APPROVED=false
echo "Selected ADO run was canceled; continuing with replacement build $BUILD_ID"
continue
fi
ARTIFACT_FAILURES=0
echo "Canceled build $BUILD_ID has no replacement yet..."
SLEEP_SECONDS=$((DEADLINE - SECONDS))
if (( SLEEP_SECONDS > 30 )); then SLEEP_SECONDS=30; fi
if (( SLEEP_SECONDS > 0 )); then sleep "$SLEEP_SECONDS"; fi
continue
fi
if [[ -n "$COVERAGE_ARTIFACT" ]] &&
{ [[ "$STATUS" == "inProgress" ]] ||
{ [[ "$STATUS" == "completed" ]] &&
[[ "$RESULT" =~ ^(succeeded|partiallySucceeded|failed)$ ]]; }; }; then
COVERAGE_ARTIFACT_APPROVED=true
echo "βœ… Found coverage artifact!"
break
fi
if [[ "$STATUS" == "completed" ]] && (( COMPLETED_AT < 0 )); then
COMPLETED_AT=$SECONDS
echo "Build completed ($RESULT); allowing 2 minutes for artifact propagation..."
fi
else
BUILD_FAILURES=$((BUILD_FAILURES + 1))
echo "⚠️ Build lifecycle API HTTP/JSON error ($BUILD_FAILURES/5)"
if (( BUILD_FAILURES >= 5 )); then
echo "❌ Azure DevOps build lifecycle API unavailable after 5 consecutive failures"
exit 1
fi
fi
if (( COMPLETED_AT >= 0 && SECONDS - COMPLETED_AT >= 120 )); then
echo "❌ Build $BUILD_ID completed but coverage artifact is still unavailable after propagation grace"
exit 1
fi
echo "⏳ Coverage report not ready yet..."
SLEEP_SECONDS=$((DEADLINE - SECONDS))
if (( SLEEP_SECONDS > 30 )); then SLEEP_SECONDS=30; fi
if (( SLEEP_SECONDS > 0 )); then sleep "$SLEEP_SECONDS"; fi
done
if [[ "$COVERAGE_ARTIFACT_APPROVED" != true || -z "$COVERAGE_ARTIFACT" ]]; then
echo "❌ Timeout: Coverage report artifact not found within 220 minutes"
exit 1
fi
if [[ -n "$COVERAGE_ARTIFACT" && "$COVERAGE_ARTIFACT" != "null" && "$COVERAGE_ARTIFACT" != "empty" ]]; then
echo "πŸ“Š Downloading coverage report..."
COVERAGE_ARCHIVE="$RUNNER_TEMP/coverage-report.zip"
if ! curl -L "$COVERAGE_ARTIFACT" -o "$COVERAGE_ARCHIVE" --fail --silent --show-error \
--connect-timeout 10 --max-time 60 --max-filesize 268435456 \
--retry 2 --retry-delay 5 --retry-max-time 180; 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
INDEX_FILE="$RUNNER_TEMP/coverage-index.html"
if ! python .github/scripts/extract_coverage_artifact.py html "$COVERAGE_ARCHIVE" "$INDEX_FILE"; then
echo "❌ Failed to read the coverage HTML artifact"
exit 1
fi
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
LOW_COVERAGE_FILES="${LOW_COVERAGE_FILES:-No detailed file data available}"
LOW_COVERAGE_DELIMITER="coverage_$(openssl rand -hex 16)"
if grep -Fxq "$LOW_COVERAGE_DELIMITER" <<< "$LOW_COVERAGE_FILES"; then
echo "Generated delimiter unexpectedly occurs in coverage data"
exit 1
fi
{
echo "LOW_COVERAGE_FILES<<$LOW_COVERAGE_DELIMITER"
echo "$LOW_COVERAGE_FILES"
echo "$LOW_COVERAGE_DELIMITER"
} >> "$GITHUB_ENV"
echo "βœ… Coverage data extracted successfully"
else
echo "❌ Could not find index.html in coverage report"
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..."
for i in {1..5}; do
if ARTIFACTS_RESPONSE=$(curl --fail --silent --show-error --connect-timeout 10 --max-time 30 "$ARTIFACTS_URL") &&
jq -e 'type == "object" and (.value | type == "array")' <<< "$ARTIFACTS_RESPONSE" >/dev/null 2>&1; then
break
fi
echo "⚠️ Artifacts API HTTP/JSON error ($i/5)"
if [[ $i -eq 5 ]]; then
echo "❌ Azure DevOps artifacts API unavailable after 5 attempts"
exit 1
fi
sleep 5
done
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"
COVERAGE_XML_ARCHIVE="$RUNNER_TEMP/coverage-artifacts.zip"
if ! curl -L "$COVERAGE_XML_ARTIFACT" -o "$COVERAGE_XML_ARCHIVE" --fail --silent --show-error \
--connect-timeout 10 --max-time 60 --max-filesize 268435456 \
--retry 2 --retry-delay 5 --retry-max-time 180; then
echo "❌ Failed to download coverage artifacts"
exit 1
fi
COVERAGE_XML="$RUNNER_TEMP/coverage.xml"
if ! python .github/scripts/extract_coverage_artifact.py xml "$COVERAGE_XML_ARCHIVE" "$COVERAGE_XML"; then
echo "❌ Failed to read the coverage XML artifact"
exit 1
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
env:
COVERAGE_XML: ${{ runner.temp }}/coverage.xml
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)
if [[ ! "$PATCH_COVERAGE" =~ ^[0-9]+([.][0-9]+)?$ ]]; then
PATCH_COVERAGE="N/A"
fi
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
PATCH_COVERAGE_DELIMITER="coverage_$(openssl rand -hex 16)"
if grep -Fxq "$PATCH_COVERAGE_DELIMITER" patch-coverage.md; then
echo "Generated delimiter unexpectedly occurs in patch coverage data"
exit 1
fi
{
echo "PATCH_COVERAGE_SUMMARY<<$PATCH_COVERAGE_DELIMITER"
cat patch-coverage.md
echo "$PATCH_COVERAGE_DELIMITER"
} >> "$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: Save coverage data for comment
run: |
mkdir -p coverage-comment-data
jq -n \
--arg coverage_percentage "${{ env.COVERAGE_PERCENTAGE }}" \
--arg covered_lines "${{ env.COVERED_LINES }}" \
--arg total_lines "${{ env.TOTAL_LINES }}" \
--arg patch_coverage_pct "${{ env.PATCH_COVERAGE_PCT }}" \
--arg low_coverage_files "$LOW_COVERAGE_FILES" \
--arg ado_url "${{ env.ADO_URL }}" \
'{
coverage_percentage: $coverage_percentage,
covered_lines: $covered_lines,
total_lines: $total_lines,
patch_coverage_pct: $patch_coverage_pct,
low_coverage_files: $low_coverage_files,
ado_url: $ado_url
}' > coverage-comment-data/pr-info.json
# Validate JSON before uploading
echo "Validating generated JSON..."
jq . coverage-comment-data/pr-info.json > /dev/null || {
echo "❌ Invalid JSON generated"
cat coverage-comment-data/pr-info.json
exit 1
}
echo "βœ… JSON validation successful"
cat coverage-comment-data/pr-info.json
- name: Upload coverage comment data
# Only upload artifact for forked PRs since same-repo PRs post comment directly
# This prevents unnecessary workflow_run triggers for same-repo PRs
if: github.event.pull_request.head.repo.full_name != github.repository
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: coverage-comment-data
path: coverage-comment-data/
retention-days: 7
- name: Comment coverage summary on PR
# Skip for forked PRs due to token permission restrictions
if: github.event.pull_request.head.repo.full_name == github.repository
uses: ./.github/actions/post-coverage-comment
with:
pr_number: ${{ github.event.pull_request.number }}
coverage_percentage: ${{ env.COVERAGE_PERCENTAGE }}
covered_lines: ${{ env.COVERED_LINES }}
total_lines: ${{ env.TOTAL_LINES }}
patch_coverage_pct: ${{ env.PATCH_COVERAGE_PCT }}
low_coverage_files: ${{ env.LOW_COVERAGE_FILES }}
patch_coverage_summary: ${{ env.PATCH_COVERAGE_SUMMARY }}
ado_url: ${{ env.ADO_URL }}