Add PHPUnit Coverage workflow #37
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |