This commit is contained in:
Pearl Dsilva 2026-05-12 08:17:45 +01:00 committed by GitHub
commit 8306880505
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 229 additions and 1 deletions

View File

@ -21,6 +21,7 @@ on: [pull_request, push]
permissions:
contents: read
pull-requests: write # required to post/update the grade comment on PRs
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
@ -28,7 +29,7 @@ concurrency:
jobs:
build:
if: github.repository == 'apache/cloudstack'
if: github.repository == 'apache/cloudstack' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)
name: codecov
runs-on: ubuntu-22.04
steps:
@ -57,3 +58,56 @@ jobs:
verbose: true
name: codecov
token: ${{ secrets.CODECOV_TOKEN }}
- name: Compute Coverage Grade
id: grade
run: bash scripts/coverage-grade.sh client/target/site/jacoco-aggregate/jacoco.xml
# Posts a new comment on every push so coverage history is preserved across the PR timeline.
# On push events (no PR number) this step is skipped automatically.
- name: Post Coverage Grade Comment on PR
if: github.event_name == 'pull_request'
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const grade = '${{ steps.grade.outputs.coverage_grade }}';
const label = '${{ steps.grade.outputs.coverage_grade_label }}';
const linePct = '${{ steps.grade.outputs.line_coverage }}';
const branchPct = '${{ steps.grade.outputs.branch_coverage }}';
const emojiMap = { A: '🟢', B: '🟡', C: '🟠', D: '🔴', F: '⛔' };
const emoji = emojiMap[grade] ?? '❓';
const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
const branchRow = branchPct !== 'N/A'
? `| Branch coverage | **${branchPct}%** |`
: '';
const body = [
`## ${emoji} Test Coverage Grade: \`${grade}\` — ${label}`,
'',
'| Metric | Value |',
'|--------|-------|',
`| Line coverage | **${linePct}%** |`,
branchRow,
'',
'### Grade Scale',
'| Grade | Line Coverage | Meaning |',
'|-------|--------------|---------|',
'| 🟢 A | ≥ 80% | Excellent - this code sleeps well at night 😴 |',
'| 🟡 B | 60-79% | Good - almost there, don\'t stop now 😉 |',
'| 🟠 C | 40-59% | Acceptable - your code is wearing a seatbelt, but no airbags 😬 |',
'| 🔴 D | 20-39% | Marginal - boldly shipping where no test has gone before 🖖 |',
'| ⛔ F | < 20% | Failing - tests? what tests? 🔥 |',
'',
'> Branch coverage is shown as a secondary signal. Grade is determined by **line coverage**.',
`> [View full Actions run](${runUrl})`,
].filter(l => l !== undefined).join('\n');
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: body,
});
console.log('Posted coverage grade comment');

174
scripts/coverage-grade.sh Executable file
View File

@ -0,0 +1,174 @@
#!/usr/bin/env bash
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
#
# coverage-grade.sh
#
# Parses the JaCoCo aggregate XML report and outputs an AF coverage grade.
#
# Usage:
# ./scripts/coverage-grade.sh [path/to/jacoco.xml]
#
# Exit codes:
# 0 grade is D or above (line coverage >= 20%)
# 1 grade is F (line coverage < 20%)
#
# Environment variables (optional, used when writing GitHub outputs):
# GITHUB_OUTPUT set automatically by GitHub Actions
# GITHUB_STEP_SUMMARY set automatically by GitHub Actions
set -euo pipefail
JACOCO_XML="${1:-client/target/site/jacoco-aggregate/jacoco.xml}"
if [[ ! -f "$JACOCO_XML" ]]; then
echo "ERROR: JaCoCo report not found at: $JACOCO_XML" >&2
exit 2
fi
# ---------------------------------------------------------------------------
# Parse LINE and BRANCH counters from the top-level <report> element using
# Python's built-in xml.etree.ElementTree (no extra dependencies needed).
# ---------------------------------------------------------------------------
read -r LINE_COVERED LINE_MISSED BRANCH_COVERED BRANCH_MISSED < <(python3 - "$JACOCO_XML" <<'PYEOF'
import sys, xml.etree.ElementTree as ET
tree = ET.parse(sys.argv[1])
root = tree.getroot()
lc = lm = bc = bm = 0
# Sum counters from all <package> children so we get the true aggregate,
# avoiding any duplicate top-level counter that some JaCoCo versions emit.
for pkg in root.iter('package'):
for counter in pkg.findall('counter'):
t = counter.get('type')
if t == 'LINE':
lc += int(counter.get('covered', 0))
lm += int(counter.get('missed', 0))
elif t == 'BRANCH':
bc += int(counter.get('covered', 0))
bm += int(counter.get('missed', 0))
print(lc, lm, bc, bm)
PYEOF
)
# ---------------------------------------------------------------------------
# Compute percentages
# ---------------------------------------------------------------------------
line_total=$(( LINE_COVERED + LINE_MISSED ))
branch_total=$(( BRANCH_COVERED + BRANCH_MISSED ))
if (( line_total == 0 )); then
echo "ERROR: No LINE counters found in $JACOCO_XML was the build run with -P quality?" >&2
exit 2
fi
# Use awk for floating-point arithmetic
LINE_PCT=$(awk "BEGIN { printf \"%.2f\", ($LINE_COVERED / $line_total) * 100 }")
if (( branch_total > 0 )); then
BRANCH_PCT=$(awk "BEGIN { printf \"%.2f\", ($BRANCH_COVERED / $branch_total) * 100 }")
else
BRANCH_PCT="N/A"
fi
# ---------------------------------------------------------------------------
# Assign grade based on LINE coverage
#
# A ≥ 80% Excellent
# B 6079% Good
# C 4059% Acceptable
# D 2039% Marginal (meets minimum gate)
# F < 20% Failing
# ---------------------------------------------------------------------------
LINE_INT=$(awk "BEGIN { printf \"%d\", $LINE_PCT }") # truncate, not round
if (( LINE_INT >= 80 )); then GRADE="A"; EMOJI="🟢"; LABEL="Excellent"
elif (( LINE_INT >= 60 )); then GRADE="B"; EMOJI="🟡"; LABEL="Good"
elif (( LINE_INT >= 40 )); then GRADE="C"; EMOJI="🟠"; LABEL="Acceptable"
elif (( LINE_INT >= 20 )); then GRADE="D"; EMOJI="🔴"; LABEL="Marginal"
else GRADE="F"; EMOJI="⛔"; LABEL="Failing"
fi
# ---------------------------------------------------------------------------
# Human-readable output (always printed to stdout)
# ---------------------------------------------------------------------------
echo "┌─────────────────────────────────────────────────┐"
echo "│ CloudStack Test Coverage Report │"
echo "├─────────────────────────────────────────────────┤"
printf "│ Grade : %s %-5s %-20s │\n" "$EMOJI" "$GRADE" "($LABEL)"
printf "│ Line coverage: %6s%% (%d / %d lines)%*s│\n" \
"$LINE_PCT" "$LINE_COVERED" "$line_total" \
$(( 14 - ${#LINE_COVERED} - ${#line_total} )) " "
if [[ "$BRANCH_PCT" != "N/A" ]]; then
printf "│ Branch cov. : %6s%% (%d / %d branches)%*s│\n" \
"$BRANCH_PCT" "$BRANCH_COVERED" "$branch_total" \
$(( 11 - ${#BRANCH_COVERED} - ${#branch_total} )) " "
else
printf "│ Branch cov. : N/A (no branch data) │\n"
fi
echo "└─────────────────────────────────────────────────┘"
echo ""
echo "Grade scale: A ≥80% B 60-79% C 40-59% D 20-39% F <20% (line coverage)"
# ---------------------------------------------------------------------------
# GitHub Actions: write outputs and step summary
# ---------------------------------------------------------------------------
if [[ -n "${GITHUB_OUTPUT:-}" ]]; then
{
echo "coverage_grade=$GRADE"
echo "coverage_grade_label=$LABEL"
echo "line_coverage=$LINE_PCT"
echo "branch_coverage=$BRANCH_PCT"
} >> "$GITHUB_OUTPUT"
fi
if [[ -n "${GITHUB_STEP_SUMMARY:-}" ]]; then
{
echo "## $EMOJI Test Coverage Grade: **$GRADE** — $LABEL"
echo ""
echo "| Metric | Covered | Total | Percentage |"
echo "|--------|---------|-------|------------|"
echo "| Line coverage | $LINE_COVERED | $line_total | **${LINE_PCT}%** |"
if [[ "$BRANCH_PCT" != "N/A" ]]; then
echo "| Branch coverage | $BRANCH_COVERED | $branch_total | **${BRANCH_PCT}%** |"
fi
echo ""
echo "### Grade Scale"
echo "| Grade | Line Coverage | Meaning |"
echo "|-------|--------------|---------|"
echo "| 🟢 A | ≥ 80% | Excellent - this code sleeps well at night 😴 |"
echo "| 🟡 B | 60-79% | Good - almost there, don't stop now 😉 |"
echo "| 🟠 C | 40-59% | Acceptable - your code is wearing a seatbelt, but no airbags 😬 |"
echo "| 🔴 D | 20-39% | Marginal - boldly shipping where no test has gone before 🖖 |"
echo "| ⛔ F | < 20% | tests? what tests? 🔥 |"
echo ""
echo "> Branch coverage is shown as a secondary signal. Grade is based on line coverage."
} >> "$GITHUB_STEP_SUMMARY"
fi
# ---------------------------------------------------------------------------
# Exit non-zero for grade F so the CI job can be configured to fail
# ---------------------------------------------------------------------------
if [[ "$GRADE" == "F" ]]; then
echo ""
echo "⛔ FAIL: Line coverage ${LINE_PCT}% is below the minimum threshold of 20%." >&2
exit 1
fi
exit 0