2929 run : flutter analyze
3030
3131 - name : Run tests
32- run : flutter test --machine > test-results.json
32+ run : flutter test --machine --coverage > test-results.json
3333
3434 - name : Publish test results
3535 uses : dorny/test-reporter@v1
@@ -39,92 +39,93 @@ jobs:
3939 path : test-results.json
4040 reporter : flutter-json
4141
42- - name : Parse test results
42+ - name : Upload coverage
43+ uses : actions/upload-artifact@v4
4344 if : always()
44- id : test_summary
45+ with :
46+ name : flutter-coverage
47+ path : coverage/lcov.info
48+ retention-days : 1
49+
50+ coverage-comment :
51+ runs-on : ubuntu-latest
52+ needs : lint-dart
53+ if : always() && github.event_name == 'pull_request'
54+
55+ steps :
56+ - name : Download coverage
57+ uses : actions/download-artifact@v4
58+ with :
59+ name : flutter-coverage
60+ path : coverage
61+ continue-on-error : true
62+
63+ - name : Parse coverage
64+ id : coverage
4565 run : |
46- if [ -f test-results.json ]; then
47- # Count total tests and failures from the JSON output
48- TOTAL=$(grep -c '"type":"testStart"' test-results.json 2>/dev/null || echo 0)
49- FAILED=$(grep -c '"result":"error"' test-results.json 2>/dev/null || echo 0)
50-
51- # Ensure variables are numeric (strip whitespace and default to 0)
52- TOTAL=${TOTAL:-0}
53- FAILED=${FAILED:-0}
54- PASSED=$((TOTAL - FAILED))
55-
56- # Calculate percentage
57- if [ "$TOTAL" -gt 0 ] 2>/dev/null; then
58- PASS_RATE=$(awk "BEGIN {printf \"%.1f\", ($PASSED/$TOTAL)*100}")
59- else
60- PASS_RATE="0.0"
61- fi
62-
63- echo "total=$TOTAL" >> $GITHUB_OUTPUT
64- echo "passed=$PASSED" >> $GITHUB_OUTPUT
65- echo "failed=$FAILED" >> $GITHUB_OUTPUT
66- echo "pass_rate=$PASS_RATE" >> $GITHUB_OUTPUT
66+ if [ -f coverage/lcov.info ]; then
67+ # Use lcov to get summary (more reliable than grep)
68+ COVERAGE_SUMMARY=$(lcov --summary coverage/lcov.info 2>&1)
69+
70+ # Extract line coverage percentage
71+ COVERAGE_PCT=$(echo "$COVERAGE_SUMMARY" | grep -oP 'lines\.*: \K[\d.]+(?=%)')
72+ COVERAGE_PCT=${COVERAGE_PCT:-0.0}
73+
74+ # Extract hit/total counts
75+ LINES_HIT=$(grep -o 'DA:' coverage/lcov.info | wc -l)
76+ LINES_TOTAL=$(grep -o 'LF:' coverage/lcov.info | wc -l)
77+
78+ echo "coverage_pct=$COVERAGE_PCT" >> $GITHUB_OUTPUT
79+ echo "lines_hit=$LINES_HIT" >> $GITHUB_OUTPUT
80+ echo "lines_total=$LINES_TOTAL" >> $GITHUB_OUTPUT
6781 else
68- echo "total=0" >> $GITHUB_OUTPUT
69- echo "passed=0" >> $GITHUB_OUTPUT
70- echo "failed=0" >> $GITHUB_OUTPUT
71- echo "pass_rate=0.0" >> $GITHUB_OUTPUT
82+ echo "coverage_pct=0.0" >> $GITHUB_OUTPUT
83+ echo "lines_hit=0" >> $GITHUB_OUTPUT
84+ echo "lines_total=0" >> $GITHUB_OUTPUT
7285 fi
7386
74- - name : Comment test results
75- if : always()
87+ - name : Post coverage comment
7688 uses : actions/github-script@v7
7789 with :
7890 script : |
79- const total = '${{ steps.test_summary .outputs.total }}';
80- const passed = '${{ steps.test_summary .outputs.passed }}';
81- const failed = '${{ steps.test_summary .outputs.failed }}';
82- const passRate = '${{ steps.test_summary.outputs.pass_rate }}' ;
91+ const coveragePct = '${{ steps.coverage .outputs.coverage_pct }}';
92+ const linesHit = '${{ steps.coverage .outputs.lines_hit }}';
93+ const linesTotal = '${{ steps.coverage .outputs.lines_total }}';
94+ const linesMissing = parseInt(linesTotal) - parseInt(linesHit) ;
8395
84- const emoji = failed === '0' ? '✅' : '❌';
85- const status = failed === '0' ? 'All tests passed!' : `${failed} test(s) failed`;
86-
87- const body = `## ${emoji} Test Results
88-
89- **Status:** ${status}
96+ const body = `## 📊 Coverage Report
9097
9198 | Metric | Value |
9299 |--------|-------|
93- | Total Tests | ${total} |
94- | Passed | ✅ ${passed} |
95- | Failed | ❌ ${failed} |
96- | Pass Rate | ${passRate}% |
97-
98- ${failed !== '0' ? '⚠️ Please check the **Unit Tests** check run for detailed failure information.' : ''}
100+ | **Coverage** | **${coveragePct}%** |
101+ | Lines covered | ${linesHit} / ${linesTotal} |
102+ | Lines missing | ${linesMissing} |
99103
100- <sub>🤖 Automated test results from [checks workflow](${context.payload.repository.html_url}/actions/runs/${context.runId})</sub>`;
104+ <sub>🤖 Coverage report from [checks workflow](${context.payload.repository.html_url}/actions/runs/${context.runId})</sub>`;
101105
102106 // Find existing comment
103- const comments = await github.rest.issues.listComments({
107+ const { data: comments } = await github.rest.issues.listComments({
104108 owner: context.repo.owner,
105109 repo: context.repo.repo,
106110 issue_number: context.issue.number,
107111 });
108112
109- const botComment = comments.data.find(comment =>
110- comment.user.type === 'Bot' &&
111- comment.body.includes('## ✅ Test Results') || comment.body.includes('## ❌ Test Results')
113+ const existing = comments.find(c =>
114+ c.user.type === 'Bot' && c.body.includes('## 📊 Coverage Report')
112115 );
113116
114- if (botComment) {
115- // Update existing comment
117+ if (existing) {
116118 await github.rest.issues.updateComment({
117119 owner: context.repo.owner,
118120 repo: context.repo.repo,
119- comment_id: botComment .id,
120- body: body
121+ comment_id: existing .id,
122+ body,
121123 });
122124 } else {
123- // Create new comment
124125 await github.rest.issues.createComment({
125126 owner: context.repo.owner,
126127 repo: context.repo.repo,
127128 issue_number: context.issue.number,
128- body: body
129+ body,
129130 });
130131 }
0 commit comments