Skip to content

Add PHPUnit Coverage workflow #37

Add PHPUnit Coverage workflow

Add PHPUnit Coverage workflow #37

Workflow file for this run

name: Code Coverage
on:
pull_request:
branches:
- develop
- main
push:
branches:
- develop
- main
workflow_dispatch:
# Cancels all previous workflow runs for the same branch that have not yet completed.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
code-coverage:
name: Code Coverage Check
runs-on: ubuntu-latest
services:
mysql:
image: mysql:8.0
env:
MYSQL_ALLOW_EMPTY_PASSWORD: false
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: wordpress_tests
ports:
- 3306:3306
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=10s --health-retries=10
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0 # Fetch all history for accurate diff
- name: Install PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
ini-values: zend.assertions=1, error_reporting=-1, display_errors=On, memory_limit=512M
coverage: xdebug # Use Xdebug for coverage (PCOV was causing PHP to crash)
tools: composer
- name: Install SVN
run: sudo apt-get install subversion
- name: Install Composer dependencies
uses: ramsey/composer-install@v2
with:
dependency-versions: "highest"
composer-options: "--prefer-dist --with-dependencies"
custom-cache-suffix: $(date -u -d "-0 month -$(($(date +%d)-1)) days" "+%F")-codecov-v2
- name: Install WordPress Test Suite
shell: bash
run: tests/bin/install-wp-tests.sh wordpress_tests root root 127.0.0.1:3306 latest
- name: Generate code coverage report for current branch
run: |
echo "=== Debug: PHP Configuration ==="
php -i | grep -E "(memory_limit|max_execution_time|xdebug)"
echo "=== Debug: Check Xdebug is loaded ==="
php -m | grep xdebug || echo "Xdebug not loaded"
php -r "var_dump(extension_loaded('xdebug'));"
echo "=== Running PHPUnit with coverage ==="
echo "Start time: $(date)"
echo "Memory before: $(free -h | grep Mem)"
# Run PHPUnit and save output to file, allow test failures
set +e
php -d memory_limit=512M -d max_execution_time=300 \
vendor/bin/phpunit --configuration phpunit.xml.dist \
--coverage-clover=coverage.xml \
--coverage-text \
--verbose > phpunit-output.log 2>&1
PHPUNIT_EXIT=$?
set -e
echo "End time: $(date)"
echo "Memory after: $(free -h | grep Mem)"
echo "=== Debug: PHPUnit exit code: $PHPUNIT_EXIT ==="
echo "=== Note: Exit code $PHPUNIT_EXIT (0=success, 1=test failures, 2=errors, >128=signal termination) ==="
echo "=== Debug: Line count of PHPUnit output ==="
wc -l phpunit-output.log
echo "=== Debug: Last 100 lines of PHPUnit output ==="
tail -100 phpunit-output.log
echo "=== Debug: After running PHPUnit ==="
ls -la coverage* 2>/dev/null || echo "No coverage files in current directory"
echo "=== Checking if coverage report was generated ==="
if [ -f coverage.xml ]; then
echo "SUCCESS: coverage.xml exists!"
ls -lh coverage.xml
echo "First 20 lines of coverage.xml:"
head -20 coverage.xml
else
echo "FAIL: coverage.xml was not generated"
echo "=== Checking for errors in PHPUnit output ==="
grep -i "error\|fatal\|exception\|segfault\|out of memory" phpunit-output.log || echo "No obvious errors found"
fi
continue-on-error: false
- name: Upload PHPUnit output for debugging
if: always()
uses: actions/upload-artifact@v4
with:
name: phpunit-output
path: phpunit-output.log
retention-days: 7
- name: Generate coverage report summary
id: coverage
run: |
# Debug: Check for coverage files
echo "=== Debug: Looking for coverage files ==="
ls -la coverage* 2>/dev/null || echo "No coverage files found in current directory"
echo "=== Debug: Current directory ==="
pwd
echo "=== Debug: Workspace directory ==="
echo "${{ github.workspace }}"
# Search for coverage.xml anywhere in the workspace
echo "=== Searching for coverage.xml recursively ==="
FOUND_FILES=$(find ${{ github.workspace }} -name "coverage.xml" -type f 2>/dev/null)
if [ -n "$FOUND_FILES" ]; then
echo "Found coverage.xml files:"
echo "$FOUND_FILES"
else
echo "No coverage.xml found anywhere in workspace"
fi
# Try to find coverage.xml in multiple locations
COVERAGE_FILE=""
if [ -f coverage.xml ]; then
COVERAGE_FILE="coverage.xml"
echo "Found coverage.xml in current directory"
elif [ -f ${{ github.workspace }}/coverage.xml ]; then
COVERAGE_FILE="${{ github.workspace }}/coverage.xml"
echo "Found coverage.xml in workspace root"
else
# Try to find it anywhere
COVERAGE_FILE=$(find ${{ github.workspace }} -name "coverage.xml" -type f 2>/dev/null | head -1)
if [ -n "$COVERAGE_FILE" ]; then
echo "Found coverage.xml at: $COVERAGE_FILE"
fi
fi
if [ -n "$COVERAGE_FILE" ]; then
echo "=== Debug: Found coverage file at $COVERAGE_FILE ==="
echo "=== Debug: First 50 lines of coverage.xml ==="
head -50 "$COVERAGE_FILE"
# Extract coverage using various possible attribute names
# Try 'statements' and 'coveredstatements'
LINES=$(grep -o 'statements="[0-9]*"' "$COVERAGE_FILE" | head -1 | grep -o '[0-9]*')
COVERED=$(grep -o 'coveredstatements="[0-9]*"' "$COVERAGE_FILE" | head -1 | grep -o '[0-9]*')
# Try alternative: 'elements' and 'coveredelements'
if [ -z "$LINES" ]; then
LINES=$(grep -o 'elements="[0-9]*"' "$COVERAGE_FILE" | head -1 | grep -o '[0-9]*')
COVERED=$(grep -o 'coveredelements="[0-9]*"' "$COVERAGE_FILE" | head -1 | grep -o '[0-9]*')
fi
echo "=== Debug: LINES=$LINES, COVERED=$COVERED ==="
if [ -n "$LINES" ] && [ -n "$COVERED" ] && [ "$LINES" -gt 0 ]; then
COVERAGE=$(awk "BEGIN {printf \"%.2f\", ($COVERED/$LINES)*100}")
else
COVERAGE="0"
fi
else
echo "=== Debug: No coverage.xml file found ==="
COVERAGE="0"
fi
echo "current_coverage=$COVERAGE" >> $GITHUB_OUTPUT
echo "Current code coverage: $COVERAGE%"
- name: Checkout base branch for comparison
if: github.event_name == 'pull_request'
run: |
# Save coverage.xml before switching branches
cp coverage.xml /tmp/current-coverage.xml 2>/dev/null || true
# Stash any local changes (like composer.lock)
git stash --include-untracked || true
git fetch origin ${{ github.base_ref }}
git checkout origin/${{ github.base_ref }}
- name: Install dependencies on base branch
if: github.event_name == 'pull_request'
run: |
composer install --no-interaction --prefer-dist --optimize-autoloader
- name: Generate coverage report for base branch
if: github.event_name == 'pull_request'
id: base_coverage
run: |
# Generate coverage for base branch
vendor/bin/phpunit --coverage-text --colors=never > base-coverage.txt 2>&1 || true
BASE_COVERAGE=$(cat base-coverage.txt | grep "Lines:" | awk '{print $2}' | sed 's/%//' || echo "0")
echo "base_coverage=$BASE_COVERAGE" >> $GITHUB_OUTPUT
echo "Base branch code coverage: $BASE_COVERAGE%"
continue-on-error: true
- name: Compare coverage and enforce threshold
if: github.event_name == 'pull_request'
run: |
CURRENT="${{ steps.coverage.outputs.current_coverage }}"
BASE="${{ steps.base_coverage.outputs.base_coverage }}"
# Default to 0 if base coverage couldn't be determined
BASE=${BASE:-0}
echo "Current Coverage: $CURRENT%"
echo "Base Coverage: $BASE%"
# Calculate the difference
DIFF=$(echo "$CURRENT - $BASE" | bc)
echo "Coverage Difference: $DIFF%"
# Check if coverage dropped by more than 0.5%
THRESHOLD=-0.5
if (( $(echo "$DIFF < $THRESHOLD" | bc -l) )); then
echo "❌ Code coverage dropped by ${DIFF}%, which exceeds the allowed threshold of ${THRESHOLD}%"
echo "Please add tests to maintain or improve code coverage."
exit 1
else
echo "✅ Code coverage check passed!"
echo "Coverage change: ${DIFF}%"
fi
- name: Comment PR with coverage
if: github.event_name == 'pull_request'
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const current = parseFloat('${{ steps.coverage.outputs.current_coverage }}') || 0;
const base = parseFloat('${{ steps.base_coverage.outputs.base_coverage }}') || 0;
const diff = (current - base).toFixed(2);
const diffEmoji = diff >= 0 ? '📈' : '📉';
const coverageEmoji = current >= 80 ? '🎉' : current >= 60 ? '📈' : current >= 40 ? '📊' : '📉';
const status = diff >= -0.5 ? '✅' : '⚠️';
const comment = `## ${status} Code Coverage Report
| Metric | Value |
|--------|-------|
| **Total Coverage** | **${current.toFixed(2)}%** ${coverageEmoji} |
| Base Coverage | ${base.toFixed(2)}% |
| Difference | ${diffEmoji} **${diff}%** |
${current >= 40 ? '✅ Coverage meets minimum threshold (40%)' : '⚠️ Coverage below recommended 40% threshold'}
${diff < -0.5 ? '⚠️ **Warning:** Coverage dropped by more than 0.5%. Please add tests.' : ''}
${diff >= 0 ? '🎉 Great job maintaining/improving code coverage!' : ''}
_All tests run in a single job with Xdebug coverage._
🤖 Generated with [Claude Code](https://claude.com/claude-code)
`;
// Find existing coverage report comment
const {data: comments} = await github.rest.issues.listComments({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
});
const botComment = comments.find(comment =>
comment.user.type === 'Bot' &&
comment.body.includes('Code Coverage Report')
);
if (botComment) {
// Update existing comment
await github.rest.issues.updateComment({
comment_id: botComment.id,
owner: context.repo.owner,
repo: context.repo.repo,
body: comment
});
} else {
// Create new comment
await github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: comment
});
}
- name: Generate HTML coverage report
if: always()
run: |
vendor/bin/phpunit --coverage-html=coverage-html
continue-on-error: true
- name: Upload HTML coverage report as artifact
if: always()
uses: actions/upload-artifact@v4
with:
name: coverage-report
path: coverage-html/
retention-days: 30