diff --git a/.github/actions/check-coverage/action.yaml b/.github/actions/check-coverage/action.yaml index 061cc17..7f9bb90 100644 --- a/.github/actions/check-coverage/action.yaml +++ b/.github/actions/check-coverage/action.yaml @@ -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="" + 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." diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 521a153..c56ce7c 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -2,6 +2,9 @@ name: CI on: push: + branches: + - main + pull_request: workflow_dispatch: @@ -94,3 +97,4 @@ jobs: uses: ./.github/actions/check-coverage with: minimum-coverage: 90.00 + github-token: ${{ secrets.GITHUB_TOKEN }}