Skip to content

Add PHPUnit Coverage workflow #6

Add PHPUnit Coverage workflow

Add PHPUnit Coverage workflow #6

name: Code Coverage (Grouped)
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:
coverage-matrix:
name: Coverage - ${{ matrix.group }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
group:
- activities
- badges
- admin
- api
- suggested-tasks-providers-1
- suggested-tasks-providers-2
- suggested-tasks-providers-3
- suggested-tasks-providers-4
- suggested-tasks-data-collectors-1
- suggested-tasks-data-collectors-2
- suggested-tasks-data-collectors-3
- goals
- updates
- ui
- utils
- misc
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
- name: Install PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
ini-values: memory_limit=512M
coverage: xdebug
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"
- 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: Run PHPUnit with coverage for group
run: |
echo "Running coverage for group: ${{ matrix.group }}"
# Run PHPUnit with coverage using the group annotation
set +e
php -d memory_limit=512M \
vendor/bin/phpunit \
--coverage-clover=coverage-${{ matrix.group }}.xml \
--coverage-text \
--group "${{ matrix.group }}"
EXIT_CODE=$?
set -e
echo "PHPUnit exit code: $EXIT_CODE"
# Check if coverage was generated
if [ -f coverage-${{ matrix.group }}.xml ]; then
echo "✓ Coverage generated for ${{ matrix.group }}"
ls -lh coverage-${{ matrix.group }}.xml
else
echo "✗ Coverage NOT generated for ${{ matrix.group }}"
exit 1
fi
- name: Upload coverage for group
uses: actions/upload-artifact@v4
with:
name: coverage-${{ matrix.group }}
path: coverage-${{ matrix.group }}.xml
retention-days: 1
merge-coverage:
name: Merge & Report Coverage
needs: coverage-matrix
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
tools: composer
- name: Download all coverage artifacts
uses: actions/download-artifact@v4
with:
pattern: coverage-*
merge-multiple: true
- name: List downloaded coverage files
run: |
echo "Downloaded coverage files:"
ls -lh coverage-*.xml
- name: Merge coverage reports
run: |
# Merge all Clover XML files into a single report
php -r '
$xmlFiles = glob("coverage-*.xml");
if (empty($xmlFiles)) {
echo "No coverage files found\n";
exit(1);
}
$totalLines = 0;
$coveredLines = 0;
$files = [];
foreach ($xmlFiles as $xmlFile) {
$xml = simplexml_load_file($xmlFile);
if ($xml === false) {
echo "Failed to parse $xmlFile\n";
continue;
}
// Extract metrics from each file
foreach ($xml->xpath("//file") as $file) {
$path = (string)$file["name"];
$metrics = $file->metrics[0];
if (!isset($files[$path])) {
$files[$path] = [
"elements" => (int)$metrics["elements"],
"coveredelements" => (int)$metrics["coveredelements"],
"statements" => (int)$metrics["statements"],
"coveredstatements" => (int)$metrics["coveredstatements"],
];
}
}
// Get project totals
$project = $xml->project[0];
if ($project && $project->metrics[0]) {
$metrics = $project->metrics[0];
$totalLines += (int)$metrics["statements"];
$coveredLines += (int)$metrics["coveredstatements"];
}
}
echo "Processed " . count($xmlFiles) . " coverage files\n";
echo "Total statements: $totalLines\n";
echo "Covered statements: $coveredLines\n";
if ($totalLines > 0) {
$percentage = round(($coveredLines / $totalLines) * 100, 2);
echo "Coverage: $percentage%\n";
// Save coverage percentage for next step
file_put_contents("coverage-summary.txt", $percentage);
} else {
echo "No coverage data found\n";
file_put_contents("coverage-summary.txt", "0");
}
'
echo "Merged coverage summary saved"
- name: Generate coverage summary
id: coverage
run: |
if [ ! -f coverage-summary.txt ]; then
echo "current_coverage=0" >> $GITHUB_OUTPUT
echo "Current code coverage: 0%"
exit 0
fi
COVERAGE=$(cat coverage-summary.txt)
echo "current_coverage=$COVERAGE" >> $GITHUB_OUTPUT
echo "Current code coverage: $COVERAGE%"
- name: Save current coverage
if: github.event_name == 'pull_request'
run: |
cp coverage-summary.txt /tmp/current-coverage.txt
echo "Saved current coverage: $(cat coverage-summary.txt)%"
- 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 }}');
const emoji = current >= 80 ? '🎉' : current >= 60 ? '📈' : current >= 40 ? '📊' : '📉';
const status = current >= 40 ? '✅' : '⚠️';
const comment = `## ${status} Code Coverage Report
| Metric | Value |
|--------|-------|
| **Total Coverage** | **${current.toFixed(2)}%** ${emoji} |
Coverage collected from **18 test groups** running in parallel:
- activities, badges, admin, api, goals, updates, ui, utils, misc
- suggested-tasks-providers (1-4)
- suggested-tasks-data-collectors (1-3)
${current >= 40 ? '✅ Coverage meets minimum threshold' : '⚠️ Coverage below recommended 40% threshold'}
_Tests are grouped to avoid memory/timeout issues. Each group runs independently with Xdebug coverage._
🤖 Generated with [Claude Code](https://claude.com/claude-code)
`;
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) {
await github.rest.issues.updateComment({
comment_id: botComment.id,
owner: context.repo.owner,
repo: context.repo.repo,
body: comment
});
} else {
await github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: comment
});
}
- name: Upload coverage summary
uses: actions/upload-artifact@v4
with:
name: coverage-summary
path: coverage-summary.txt
retention-days: 30