Skip to content

feat(repo): Generate code coverage on pull requests #2

feat(repo): Generate code coverage on pull requests

feat(repo): Generate code coverage on pull requests #2

Workflow file for this run

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