feat(repo): Generate code coverage on pull requests #2
Workflow file for this run
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: Code Coverage | |
| on: | |
| pull_request: | |
| branches: [main] | |
| push: | |
| branches: [main] | |
| workflow_dispatch: | |
| permissions: | |
| contents: read | |
| pull-requests: write | |
| defaults: | |
| run: | |
| shell: bash | |
| jobs: | |
| coverage: | |
| name: Generate code coverage with cargo-llvm-cov | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout Repository | |
| uses: actions/checkout@v4 | |
| - name: Cache cargo builds | |
| uses: Swatinem/rust-cache@v2 | |
| - name: Install Rust toolchain with llvm-tools-preview | |
| uses: dtolnay/rust-toolchain@stable | |
| with: | |
| components: llvm-tools-preview | |
| - name: Install cargo-llvm-cov | |
| uses: taiki-e/install-action@cargo-llvm-cov | |
| - name: Install Linux deps for winit/wgpu | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y \ | |
| pkg-config libx11-dev libxcb1-dev libxcb-render0-dev \ | |
| libxcb-shape0-dev libxcb-xfixes0-dev libxkbcommon-dev \ | |
| libwayland-dev libudev-dev \ | |
| libvulkan-dev libvulkan1 mesa-vulkan-drivers vulkan-tools | |
| - name: Install Linux deps for audio | |
| run: | | |
| sudo apt-get install -y libasound2-dev | |
| - name: Configure Vulkan (Ubuntu) | |
| run: | | |
| echo "WGPU_BACKEND=vulkan" >> "$GITHUB_ENV" | |
| # Prefer Mesa's software Vulkan (lavapipe) for headless availability | |
| echo "VK_ICD_FILENAMES=/usr/share/vulkan/icd.d/lvp_icd.x86_64.json" >> "$GITHUB_ENV" | |
| vulkaninfo --summary || true | |
| - name: Generate full coverage JSON | |
| run: | | |
| cargo llvm-cov --workspace \ | |
| --features lambda-rs/with-vulkan,lambda-rs/audio-output-device \ | |
| --json \ | |
| --output-path coverage.json | |
| - name: Get changed files in PR | |
| if: github.event_name == 'pull_request' | |
| id: changed | |
| run: | | |
| git fetch origin ${{ github.base_ref }} --depth=1 | |
| changed_files=$(git diff --name-only origin/${{ github.base_ref }}...HEAD -- '*.rs' | tr '\n' ' ') | |
| echo "files=$changed_files" >> "$GITHUB_OUTPUT" | |
| - name: Generate coverage report data | |
| id: cov | |
| run: | | |
| # Extract total coverage | |
| pct=$(jq -r '(.data[0].totals.lines.percent // 0)' coverage.json) | |
| covered=$(jq -r '(.data[0].totals.lines.covered // 0)' coverage.json) | |
| total=$(jq -r '(.data[0].totals.lines.count // 0)' coverage.json) | |
| echo "pct=$pct" >> "$GITHUB_OUTPUT" | |
| echo "covered=$covered" >> "$GITHUB_OUTPUT" | |
| echo "total=$total" >> "$GITHUB_OUTPUT" | |
| # Extract per-file coverage as JSON for changed files | |
| jq -r '.data[0].files[] | "\(.filename)|\(.summary.lines.percent // 0)|\(.summary.lines.covered // 0)|\(.summary.lines.count // 0)"' coverage.json > file_coverage.txt | |
| - name: Build PR coverage comment | |
| if: github.event_name == 'pull_request' | |
| id: comment | |
| env: | |
| CHANGED_FILES: ${{ steps.changed.outputs.files }} | |
| run: | | |
| # Build the comment body | |
| { | |
| echo "### ✅ Coverage Report" | |
| echo "" | |
| echo "#### Overall Coverage" | |
| echo "" | |
| echo "| Metric | Value |" | |
| echo "|--------|-------|" | |
| echo "| **Total Line Coverage** | ${{ steps.cov.outputs.pct }}% |" | |
| echo "| **Lines Covered** | ${{ steps.cov.outputs.covered }} / ${{ steps.cov.outputs.total }} |" | |
| echo "" | |
| # Calculate coverage for changed files | |
| if [ -n "$CHANGED_FILES" ]; then | |
| echo "#### Changed Files in This PR" | |
| echo "" | |
| echo "| File | Coverage | Lines |" | |
| echo "|------|----------|-------|" | |
| pr_covered=0 | |
| pr_total=0 | |
| for file in $CHANGED_FILES; do | |
| # Find this file in coverage data (match by filename ending) | |
| match=$(grep -E "/${file}\|" file_coverage.txt || grep -E "^${file}\|" file_coverage.txt || true) | |
| if [ -n "$match" ]; then | |
| file_pct=$(echo "$match" | cut -d'|' -f2) | |
| file_covered=$(echo "$match" | cut -d'|' -f3) | |
| file_total=$(echo "$match" | cut -d'|' -f4) | |
| # Format percentage to 2 decimal places | |
| file_pct_fmt=$(printf "%.2f" "$file_pct") | |
| echo "| \`${file}\` | ${file_pct_fmt}% | ${file_covered}/${file_total} |" | |
| pr_covered=$((pr_covered + file_covered)) | |
| pr_total=$((pr_total + file_total)) | |
| else | |
| echo "| \`${file}\` | N/A | (no coverage data) |" | |
| fi | |
| done | |
| echo "" | |
| if [ "$pr_total" -gt 0 ]; then | |
| pr_pct=$(echo "scale=2; $pr_covered * 100 / $pr_total" | bc) | |
| echo "**PR Files Coverage:** ${pr_pct}% (${pr_covered}/${pr_total} lines)" | |
| fi | |
| else | |
| echo "*No Rust files changed in this PR.*" | |
| fi | |
| echo "" | |
| echo "---" | |
| echo "*Generated by [cargo-llvm-cov](https://github.com/taiki-e/cargo-llvm-cov)*" | |
| } > comment_body.md | |
| # Store as output (handle multiline) | |
| { | |
| echo "body<<EOF" | |
| cat comment_body.md | |
| echo "EOF" | |
| } >> "$GITHUB_OUTPUT" | |
| - name: Find existing coverage comment | |
| if: github.event_name == 'pull_request' | |
| id: find_comment | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const { owner, repo } = context.repo; | |
| const issue_number = context.issue.number; | |
| const comments = await github.rest.issues.listComments({ | |
| owner, | |
| repo, | |
| issue_number, | |
| }); | |
| const botComment = comments.data.find(comment => | |
| comment.user.type === 'Bot' && | |
| comment.body.includes('### ✅ Coverage Report') | |
| ); | |
| return botComment ? botComment.id : null; | |
| result-encoding: string | |
| - name: Create or update PR comment | |
| if: github.event_name == 'pull_request' | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const fs = require('fs'); | |
| const body = fs.readFileSync('comment_body.md', 'utf8'); | |
| const { owner, repo } = context.repo; | |
| const issue_number = context.issue.number; | |
| const existingCommentId = ${{ steps.find_comment.outputs.result }}; | |
| if (existingCommentId) { | |
| await github.rest.issues.updateComment({ | |
| owner, | |
| repo, | |
| comment_id: existingCommentId, | |
| body, | |
| }); | |
| console.log(`Updated existing comment ${existingCommentId}`); | |
| } else { | |
| await github.rest.issues.createComment({ | |
| owner, | |
| repo, | |
| issue_number, | |
| body, | |
| }); | |
| console.log('Created new coverage comment'); | |
| } | |
| - name: Upload coverage JSON as artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: coverage-report | |
| path: coverage.json | |
| retention-days: 30 |