1+ # Generate coverage report for PRs
2+ name : Coverage
3+
4+ on :
5+ pull_request :
6+ types : [opened, synchronize, reopened, ready_for_review]
7+ branches :
8+ - main
9+
10+ concurrency :
11+ group : ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
12+ cancel-in-progress : true
13+
14+ jobs :
15+ coverage :
16+ name : tarpaulin
17+ if : github.event.pull_request.draft == false
18+ runs-on : ubuntu-latest
19+ container :
20+ image : xd009642/tarpaulin:latest
21+ options : --security-opt seccomp=unconfined
22+ permissions :
23+ contents : read
24+ pull-requests : write
25+
26+ steps :
27+ - name : Checkout PR
28+ uses : actions/checkout@v4
29+
30+ # This is required for tarpaulin caches to work
31+ - name : Cache cargo registry and build
32+ uses : actions/cache@v4
33+ with :
34+ path : |
35+ ~/.cargo/registry
36+ ~/.cargo/git
37+ target
38+ target/tarpaulin
39+ key : ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
40+ restore-keys : |
41+ ${{ runner.os }}-cargo-
42+
43+ - name : Install Protoc
44+ uses : arduino/setup-protoc@v3
45+
46+ - name : Generate coverage
47+ run : |
48+ cargo tarpaulin --skip-clean --target-dir target/tarpaulin --all-features --workspace --timeout 120 --exclude-files target/debug/build/*/out/* --fail-under 20 | tee tarpaulin_output.txt
49+ PR_COVERAGE=$(awk '/% coverage/ {print $1}' tarpaulin_output.txt)
50+ PR_COVERAGE_LINES=$(awk -F'[ ,]+' '/lines covered/ {print $3}' tarpaulin_output.txt)
51+ PR_COVERAGE_DETAILED=$(awk '/\|\| Tested\/Total Lines:/, /^\|\| *$/' tarpaulin_output.txt)
52+ echo "Output: $PR_COVERAGE $PR_COVERAGE_LINES"
53+ echo "PR_COVERAGE=$PR_COVERAGE" >> $GITHUB_ENV
54+ echo "PR_COVERAGE_LINES=$PR_COVERAGE_LINES" >> $GITHUB_ENV
55+ echo "PR_COVERAGE_DETAILED<<EOF" >> $GITHUB_ENV
56+ echo "$PR_COVERAGE_DETAILED" >> $GITHUB_ENV
57+ echo "EOF" >> $GITHUB_ENV
58+
59+ - name : Comment PR with coverage
60+ uses : actions/github-script@v7
61+ with :
62+ script : |
63+ const prCoverage = parseFloat(process.env.PR_COVERAGE);
64+ const prCoverageLines = process.env.PR_COVERAGE_LINES;
65+ const prCoverageDetailed = (process.env.PR_COVERAGE_DETAILED || "");
66+
67+ // Per-crate coverage calculation
68+ const crateStats = {};
69+ prCoverageDetailed.split('\n').forEach(line => {
70+ const match = line.match(/^\|\|\s*([^:]+):\s*(\d+)\/(\d+)/);
71+ if (match) {
72+ const filePath = match[1].trim();
73+ const covered = parseInt(match[2], 10);
74+ const total = parseInt(match[3], 10);
75+ const crate = filePath.split('/').slice(0, 2).join('/');
76+ if (!crateStats[crate]) {
77+ crateStats[crate] = { covered: 0, total: 0 };
78+ }
79+ crateStats[crate].covered += covered;
80+ crateStats[crate].total += total;
81+ }
82+ });
83+
84+ let crateTable = '| Crate | Coverage |\n|-------|----------|\n';
85+ for (const [crate, { covered, total }] of Object.entries(crateStats)) {
86+ const percent = total > 0 ? ((covered / total) * 100).toFixed(2) : 'N/A';
87+ crateTable += `| ${crate} | ${percent}% |\n`;
88+ }
89+
90+ const dedent = str => str.replace(/^[ \t]+/gm, '');
91+
92+ const comment = dedent(`## Coverage Report
93+
94+ | Metric | Value |
95+ |--------|-------|
96+ | **Coverage** | ${prCoverage.toFixed(2)}% |
97+ | **Lines Covered** | ${prCoverageLines} |
98+
99+ ${prCoverage >= 80 ? '✅' : prCoverage >= 60 ? '⚠️' : '❌'} **Status**: ${prCoverage >= 80 ? 'GOOD' : prCoverage >= 60 ? 'NEEDS IMPROVEMENT' : 'POOR'}
100+
101+ ### Per-Crate Coverage
102+
103+ ${crateTable}
104+ `);
105+
106+ github.rest.issues.createComment({
107+ issue_number: context.issue.number,
108+ owner: context.repo.owner,
109+ repo: context.repo.repo,
110+ body: comment
111+ });
0 commit comments