Skip to content

ci: check-coverage action pr commenting feature #2

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jun 30, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 96 additions & 15 deletions .github/actions/check-coverage/action.yaml
Original file line number Diff line number Diff line change
@@ -1,41 +1,122 @@
name: Check Coverage
description: Checks if test coverage meets the minimum threshold
description: >-
Checks test coverage against a minimum threshold and posts a report as a PR comment.
Requires `pull-requests: write` permission.

inputs:
lcov_file:
description: Path to the lcov.info file
description: Path to the lcov.info file.
default: lcov.info

minimum-coverage:
description: Minimum coverage threshold percentage
description: Minimum coverage threshold percentage.
default: 90.00

github-token:
description: GitHub token for API access.
required: true

runs:
using: composite
steps:
- name: Run coverage check
shell: bash
env:
GH_TOKEN: ${{ inputs.github-token }}
GITHUB_REPOSITORY: ${{ github.repository }}
run: |
# Sum total lines found (LF)
TOTAL_LINES=$(grep ^LF: "${{ inputs.lcov_file }}" | cut -d: -f2 | awk '{sum += $1} END {print sum}')
set -euo pipefail

echo "🔍 Debug: Processing coverage file \"${{ inputs.lcov_file }}\""
echo "🔍 Debug: Minimum coverage threshold: ${{ inputs.minimum-coverage }}%"
echo "🔍 Debug: GitHub event: $GITHUB_EVENT_NAME"

if [ ! -f "${{ inputs.lcov_file }}" ]; then
echo "❌ Error: Coverage file not found at \"${{ inputs.lcov_file }}\""
exit 1
fi
if ! [[ "${{ inputs.minimum-coverage }}" =~ ^[0-9]+(\.[0-9]+)?$ ]]; then
echo "❌ Error: Invalid minimum coverage value '${{ inputs.minimum-coverage }}'. Must be a number."
exit 1
fi
if ! awk -v val="${{ inputs.minimum-coverage }}" 'BEGIN { exit !(val >= 0 && val <= 100) }'; then
echo "❌ Error: Minimum coverage must be between 0 and 100, got: ${{ inputs.minimum-coverage }}"
exit 1
fi

if ! command -v jq &> /dev/null; then
echo "❌ Error: 'jq' is required but not installed. Please add it to your workflow."
exit 1
fi
if ! command -v gh &> /dev/null; then
echo "❌ Error: GitHub CLI 'gh' is required but not installed. Please add it to your workflow."
exit 1
fi

# Sum total lines hit (LH)
HIT_LINES=$(grep ^LH: "${{ inputs.lcov_file }}" | cut -d: -f2 | awk '{sum += $1} END {print sum}')
TOTAL_LINES=$(awk -F: '/^LF:/ {s+=$2} END {print s+0}' "${{ inputs.lcov_file }}")
HIT_LINES=$(awk -F: '/^LH:/ {s+=$2} END {print s+0}' "${{ inputs.lcov_file }}")

# Calculate coverage percentage (avoid division by zero)
if [ "$TOTAL_LINES" -eq 0 ]; then
LINE_COVERAGE=0
else
LINE_COVERAGE=$(echo "scale=2; $HIT_LINES*100 / $TOTAL_LINES" | bc)
echo "❌ Error: No test coverage data found in \"${{ inputs.lcov_file }}\"."
echo " This usually means either:"
echo " - No tests were executed"
echo " - The coverage file is empty"
echo " - The coverage tool didn't generate line coverage information"
exit 1
fi

echo "Line coverage: $LINE_COVERAGE%"
LINE_COVERAGE=$(awk "BEGIN {printf \"%.2f\", ($HIT_LINES / $TOTAL_LINES) * 100}")
echo "Line coverage: $LINE_COVERAGE% ($HIT_LINES / $TOTAL_LINES lines)"
PASSED=$(awk -v cov="$LINE_COVERAGE" -v min="${{ inputs.minimum-coverage }}" 'BEGIN { print (cov >= min) }')

# Compare using bc (handles float comparison)
PASSED=$(echo "$LINE_COVERAGE >= ${{ inputs.minimum-coverage }}" | bc)
if [ "$PASSED" = "1" ]; then
echo "✅ Coverage check passed ($LINE_COVERAGE%)"
STATUS_ICON="✅"; STATUS_MESSAGE="Above threshold"
echo "✅ Coverage check passed ($LINE_COVERAGE% >= ${{ inputs.minimum-coverage }}%)"
else
STATUS_ICON="❌"; STATUS_MESSAGE="Below threshold"
echo "❌ Coverage too low ($LINE_COVERAGE% < ${{ inputs.minimum-coverage }}%)"
fi

COMMENT_TAG="<!-- coverage-check-comment -->"
COMMENT_BODY=$(cat <<-EOF
### 📊 Coverage Report

| Metric | Coverage | Required | Status |
|--------|----------|----------|--------|
| Lines | \`$LINE_COVERAGE%\` | \`${{ inputs.minimum-coverage }}%\` | $STATUS_ICON $STATUS_MESSAGE |

**Details**: $HIT_LINES of $TOTAL_LINES lines covered.

$COMMENT_TAG
EOF
)

if [ "$GITHUB_EVENT_NAME" != "pull_request" ]; then
echo "⚠️ Not a pull request event. Skipping PR comment."
else
PR_NUMBER=$(jq --raw-output .pull_request.number "$GITHUB_EVENT_PATH")
if [ -z "$PR_NUMBER" ] || [ "$PR_NUMBER" = "null" ]; then
echo "❌ Error: Could not determine PR number from event payload."
exit 1
fi

echo "🔍 Searching for existing comment on PR #$PR_NUMBER..."
COMMENT_ID=$(gh api --paginate "/repos/$GITHUB_REPOSITORY/issues/$PR_NUMBER/comments" | jq --arg tag "$COMMENT_TAG" --raw-output '[.[] | select(.body | contains($tag))] | sort_by(.created_at) | last | .id')

if [ -n "$COMMENT_ID" ] && [ "$COMMENT_ID" != "null" ]; then
echo "Found previous comment (ID: $COMMENT_ID). Updating it."
BODY_JSON=$(jq -R --slurp '{body: .}' <<< "$COMMENT_BODY")
# Update the comment and redirect the successful JSON output to /dev/null to keep logs clean.
gh api --method PATCH "/repos/$GITHUB_REPOSITORY/issues/comments/$COMMENT_ID" --input - <<< "$BODY_JSON" > /dev/null
else
echo "No previous comment found. Creating a new one."
gh pr comment "$PR_NUMBER" --body "$COMMENT_BODY"
fi
fi

if [ "$PASSED" != "1" ]; then
echo "Failing the workflow because test coverage is below the threshold."
exit 1
fi

echo "✅ Coverage check completed successfully."
4 changes: 4 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ name: CI

on:
push:
branches:
- main

pull_request:
workflow_dispatch:

Expand Down Expand Up @@ -94,3 +97,4 @@ jobs:
uses: ./.github/actions/check-coverage
with:
minimum-coverage: 90.00
github-token: ${{ secrets.GITHUB_TOKEN }}