Skip to content

Commit 8bc16c1

Browse files
author
Pearl Dsilva
committed
Add code coverage grading workflow
1 parent 72b99a3 commit 8bc16c1

2 files changed

Lines changed: 232 additions & 1 deletion

File tree

.github/workflows/codecov.yml

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,15 @@ on: [pull_request, push]
2121

2222
permissions:
2323
contents: read
24+
pull-requests: write # required to post/update the grade comment on PRs
2425

2526
concurrency:
2627
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
2728
cancel-in-progress: true
2829

2930
jobs:
3031
build:
31-
if: github.repository == 'apache/cloudstack'
32+
if: github.repository == 'apache/cloudstack' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)
3233
name: codecov
3334
runs-on: ubuntu-22.04
3435
steps:
@@ -57,3 +58,56 @@ jobs:
5758
verbose: true
5859
name: codecov
5960
token: ${{ secrets.CODECOV_TOKEN }}
61+
62+
- name: Compute Coverage Grade
63+
id: grade
64+
run: bash scripts/coverage-grade.sh client/target/site/jacoco-aggregate/jacoco.xml
65+
66+
# Posts a new comment on every push so coverage history is preserved across the PR timeline.
67+
# On push events (no PR number) this step is skipped automatically.
68+
- name: Post Coverage Grade Comment on PR
69+
if: github.event_name == 'pull_request'
70+
uses: actions/github-script@v7
71+
with:
72+
github-token: ${{ secrets.GITHUB_TOKEN }}
73+
script: |
74+
const grade = '${{ steps.grade.outputs.coverage_grade }}';
75+
const label = '${{ steps.grade.outputs.coverage_grade_label }}';
76+
const linePct = '${{ steps.grade.outputs.line_coverage }}';
77+
const branchPct = '${{ steps.grade.outputs.branch_coverage }}';
78+
const emojiMap = { A: '🟢', B: '🟡', C: '🟠', D: '🔴', F: '⛔' };
79+
const emoji = emojiMap[grade] ?? '❓';
80+
const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
81+
82+
const branchRow = branchPct !== 'N/A'
83+
? `| Branch coverage | **${branchPct}%** |`
84+
: '';
85+
86+
const body = [
87+
`## ${emoji} Test Coverage Grade: \`${grade}\` — ${label}`,
88+
'',
89+
'| Metric | Value |',
90+
'|--------|-------|',
91+
`| Line coverage | **${linePct}%** |`,
92+
branchRow,
93+
'',
94+
'### Grade Scale',
95+
'| Grade | Line Coverage | Meaning |',
96+
'|-------|--------------|---------|',
97+
'| 🟢 A | ≥ 80% | Excellent |',
98+
'| 🟡 B | 60–79% | Good |',
99+
'| 🟠 C | 40–59% | Acceptable |',
100+
'| 🔴 D | 20–39% | Marginal — meets minimum gate |',
101+
'| ⛔ F | < 20% | Failing — below minimum gate |',
102+
'',
103+
'> Branch coverage is shown as a secondary signal. Grade is determined by **line coverage**.',
104+
`> [View full Actions run](${runUrl})`,
105+
].filter(l => l !== undefined).join('\n');
106+
107+
await github.rest.issues.createComment({
108+
owner: context.repo.owner,
109+
repo: context.repo.repo,
110+
issue_number: context.issue.number,
111+
body: body,
112+
});
113+
console.log('Posted coverage grade comment');

scripts/coverage-grade.sh

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
#!/usr/bin/env bash
2+
# Licensed to the Apache Software Foundation (ASF) under one
3+
# or more contributor license agreements. See the NOTICE file
4+
# distributed with this work for additional information
5+
# regarding copyright ownership. The ASF licenses this file
6+
# to you under the Apache License, Version 2.0 (the
7+
# "License"); you may not use this file except in compliance
8+
# with the License. You may obtain a copy of the License at
9+
#
10+
# http://www.apache.org/licenses/LICENSE-2.0
11+
#
12+
# Unless required by applicable law or agreed to in writing,
13+
# software distributed under the License is distributed on an
14+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
# KIND, either express or implied. See the License for the
16+
# specific language governing permissions and limitations
17+
# under the License.
18+
#
19+
# coverage-grade.sh
20+
#
21+
# Parses the JaCoCo aggregate XML report and outputs an A–F coverage grade.
22+
#
23+
# Usage:
24+
# ./scripts/coverage-grade.sh [path/to/jacoco.xml]
25+
#
26+
# Exit codes:
27+
# 0 – grade is D or above (line coverage >= 40%)
28+
# 1 – grade is F (line coverage < 40%)
29+
#
30+
# Environment variables (optional, used when writing GitHub outputs):
31+
# GITHUB_OUTPUT – set automatically by GitHub Actions
32+
# GITHUB_STEP_SUMMARY – set automatically by GitHub Actions
33+
34+
set -euo pipefail
35+
36+
JACOCO_XML="${1:-client/target/site/jacoco-aggregate/jacoco.xml}"
37+
38+
if [[ ! -f "$JACOCO_XML" ]]; then
39+
echo "ERROR: JaCoCo report not found at: $JACOCO_XML" >&2
40+
exit 2
41+
fi
42+
43+
# ---------------------------------------------------------------------------
44+
# Parse LINE and BRANCH counters from the top-level <report> element using
45+
# Python's built-in xml.etree.ElementTree (no extra dependencies needed).
46+
# ---------------------------------------------------------------------------
47+
read -r LINE_COVERED LINE_MISSED BRANCH_COVERED BRANCH_MISSED < <(python3 - "$JACOCO_XML" <<'PYEOF'
48+
import sys, xml.etree.ElementTree as ET
49+
50+
tree = ET.parse(sys.argv[1])
51+
root = tree.getroot()
52+
53+
lc = lm = bc = bm = 0
54+
# Sum counters from all <package> children so we get the true aggregate,
55+
# avoiding any duplicate top-level counter that some JaCoCo versions emit.
56+
for pkg in root.iter('package'):
57+
for counter in pkg.findall('counter'):
58+
t = counter.get('type')
59+
if t == 'LINE':
60+
lc += int(counter.get('covered', 0))
61+
lm += int(counter.get('missed', 0))
62+
elif t == 'BRANCH':
63+
bc += int(counter.get('covered', 0))
64+
bm += int(counter.get('missed', 0))
65+
66+
print(lc, lm, bc, bm)
67+
PYEOF
68+
)
69+
70+
# ---------------------------------------------------------------------------
71+
# Compute percentages
72+
# ---------------------------------------------------------------------------
73+
line_total=$(( LINE_COVERED + LINE_MISSED ))
74+
branch_total=$(( BRANCH_COVERED + BRANCH_MISSED ))
75+
76+
if (( line_total == 0 )); then
77+
echo "ERROR: No LINE counters found in $JACOCO_XML – was the build run with -P quality?" >&2
78+
exit 2
79+
fi
80+
81+
# Use awk for floating-point arithmetic
82+
LINE_PCT=$(awk "BEGIN { printf \"%.2f\", ($LINE_COVERED / $line_total) * 100 }")
83+
84+
if (( branch_total > 0 )); then
85+
BRANCH_PCT=$(awk "BEGIN { printf \"%.2f\", ($BRANCH_COVERED / $branch_total) * 100 }")
86+
else
87+
BRANCH_PCT="N/A"
88+
fi
89+
90+
# ---------------------------------------------------------------------------
91+
# Assign grade based on LINE coverage
92+
#
93+
# A ≥ 80% Excellent
94+
# B 60–79% Good
95+
# C 40–59% Acceptable
96+
# D 20–39% Marginal (meets minimum gate)
97+
# F < 20% Failing
98+
# ---------------------------------------------------------------------------
99+
LINE_INT=$(awk "BEGIN { printf \"%d\", $LINE_PCT }") # truncate, not round
100+
101+
if (( LINE_INT >= 80 )); then GRADE="A"; EMOJI="🟢"; LABEL="Excellent"
102+
elif (( LINE_INT >= 60 )); then GRADE="B"; EMOJI="🟡"; LABEL="Good"
103+
elif (( LINE_INT >= 40 )); then GRADE="C"; EMOJI="🟠"; LABEL="Acceptable"
104+
elif (( LINE_INT >= 20 )); then GRADE="D"; EMOJI="🔴"; LABEL="Marginal"
105+
else GRADE="F"; EMOJI=""; LABEL="Failing"
106+
fi
107+
108+
# ---------------------------------------------------------------------------
109+
# Human-readable output (always printed to stdout)
110+
# ---------------------------------------------------------------------------
111+
echo "┌─────────────────────────────────────────────────┐"
112+
echo "│ CloudStack Test Coverage Report │"
113+
echo "├─────────────────────────────────────────────────┤"
114+
printf "│ Grade : %s %-5s %-20s │\n" "$EMOJI" "$GRADE" "($LABEL)"
115+
printf "│ Line coverage: %6s%% (%d / %d lines)%*s│\n" \
116+
"$LINE_PCT" "$LINE_COVERED" "$line_total" \
117+
$(( 14 - ${#LINE_COVERED} - ${#line_total} )) " "
118+
if [[ "$BRANCH_PCT" != "N/A" ]]; then
119+
printf "│ Branch cov. : %6s%% (%d / %d branches)%*s│\n" \
120+
"$BRANCH_PCT" "$BRANCH_COVERED" "$branch_total" \
121+
$(( 11 - ${#BRANCH_COVERED} - ${#branch_total} )) " "
122+
else
123+
printf "│ Branch cov. : N/A (no branch data) │\n"
124+
fi
125+
echo "└─────────────────────────────────────────────────┘"
126+
echo ""
127+
echo "Grade scale: A ≥80% B 60-79% C 40-59% D 20-39% F <20% (line coverage)"
128+
129+
# ---------------------------------------------------------------------------
130+
# GitHub Actions: write outputs and step summary
131+
# ---------------------------------------------------------------------------
132+
if [[ -n "${GITHUB_OUTPUT:-}" ]]; then
133+
{
134+
echo "coverage_grade=$GRADE"
135+
echo "coverage_grade_label=$LABEL"
136+
echo "line_coverage=$LINE_PCT"
137+
echo "branch_coverage=$BRANCH_PCT"
138+
} >> "$GITHUB_OUTPUT"
139+
fi
140+
141+
if [[ -n "${GITHUB_STEP_SUMMARY:-}" ]]; then
142+
{
143+
echo "## $EMOJI Test Coverage Grade: **$GRADE** — $LABEL"
144+
echo ""
145+
echo "| Metric | Covered | Total | Percentage |"
146+
echo "|--------|---------|-------|------------|"
147+
echo "| Line coverage | $LINE_COVERED | $line_total | **${LINE_PCT}%** |"
148+
if [[ "$BRANCH_PCT" != "N/A" ]]; then
149+
echo "| Branch coverage | $BRANCH_COVERED | $branch_total | **${BRANCH_PCT}%** |"
150+
fi
151+
echo ""
152+
echo "### Grade Scale"
153+
echo "| Grade | Line Coverage | Meaning |"
154+
echo "|-------|--------------|---------|"
155+
echo "| 🟢 A | ≥ 80% | Excellent |"
156+
echo "| 🟡 B | 60–79% | Good |"
157+
echo "| 🟠 C | 40–59% | Acceptable |"
158+
echo "| 🔴 D | 20–39% | Marginal — meets minimum gate |"
159+
echo "| ⛔ F | < 20% | Failing — below minimum gate |"
160+
echo ""
161+
echo "> Branch coverage is shown as a secondary signal. Grade is based on line coverage."
162+
} >> "$GITHUB_STEP_SUMMARY"
163+
fi
164+
165+
# ---------------------------------------------------------------------------
166+
# Exit non-zero for grade F so the CI job can be configured to fail
167+
# ---------------------------------------------------------------------------
168+
if [[ "$GRADE" == "F" ]]; then
169+
echo ""
170+
echo "⛔ FAIL: Line coverage ${LINE_PCT}% is below the minimum threshold of 20%." >&2
171+
exit 1
172+
fi
173+
174+
exit 0
175+
176+
177+

0 commit comments

Comments
 (0)